diff --git a/actor/src/.gitignore b/actor/src/.gitignore new file mode 100644 index 000000000..a6f6f38f4 --- /dev/null +++ b/actor/src/.gitignore @@ -0,0 +1,3 @@ +dist +build +*.spec diff --git a/actor/src/udsactor/__init__.py b/actor/src/udsactor/__init__.py new file mode 100644 index 000000000..445bb4722 --- /dev/null +++ b/actor/src/udsactor/__init__.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +''' +@author: Adolfo Gómez, dkmaster at dkmon dot com +''' +VERSION = '3.0.0' + +__title__ = 'udsactor' +__version__ = VERSION +__build__ = 0x010756 +__author__ = 'Adolfo Gómez ' +__license__ = "BSD 3-clause" +__copyright__ = "Copyright 2014-2019 VirtualCable S.L.U." diff --git a/actor/src/udsactor/certs.py b/actor/src/udsactor/certs.py new file mode 100644 index 000000000..14f7d8141 --- /dev/null +++ b/actor/src/udsactor/certs.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +''' +@author: Adolfo Gómez, dkmaster at dkmon dot com +''' + +from tempfile import gettempdir +from os.path import exists, join + +CERTFILE = 'UDSActor.pem' + + +def createSelfSignedCert(force=False): + + certFile = join(gettempdir(), CERTFILE) + + if exists(certFile) and not force: + return certFile + + certData = '''-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCb50K3mIznNklz +yVAD7xSQOSJQ6+NPXj7U9/4zLZ+TvmbQ7RqUUsxbfxHbeRnoYTWV2nKk4+tHqmvz +ujLSS/loFhTSMqtrLn7rowSYJoQhKOUkAiQlWkqCfItWgL5pJopDpNHFul9Rn3ds +PMWQTiGeUNR4Y3RnBhr1Q1BsqAzf4m6zFUmgLPPmVLdF4uJ3Tuz8TSy2gWLs5aSr +5do4WamwUfYjRSVMJECmwjUM4rQ8SQgg0sHBeBuDUGNBvBQFac1G7qUcMReeu8Zr +DUtMsXma/l4rA8NB5CRmTrQbTBF4l+jb2BDFebDqDUK1Oqs9X35yOQfDOAFYHiix +PX0IsXOZAgMBAAECggEBAJi3000RrIUZUp6Ph0gzPMuCjDEEwWiQA7CPNX1gpb8O +dp0WhkDhUroWIaICYPSXtOwUTtVjRqivMoxPy1Thg3EIoGC/rdeSdlXRHMEGicwJ +yVyalFnatr5Xzg5wkxVh4XMd0zeDt7e3JD7s0QLo5lm1CEzd77qz6lhzFic5/1KX +bzdULtTlq60dazg2hEbcS4OmM1UMCtRVDAsOIUIZPL0M9j1C1d1iEdYnh2xshKeG +/GOfo95xsgdMlGjtv3hUT5ryKVoEsu+36rGb4VfhPfUvvoVbRx5QZpW+QvxaYh5E +Fi0JEROozFwG31Y++8El7J3yQko8cFBa1lYYUwwpNAECgYEAykT+GiM2YxJ4uVF1 +OoKiE9BD53i0IG5j87lGPnWqzEwYBwnqjEKDTou+uzMGz3MDV56UEFNho7wUWh28 +LpEkjJB9QgbsugjxIBr4JoL/rYk036e/6+U8I95lvYWrzb+rBMIkRDYI7kbQD/mQ +piYUpuCkTymNAu2RisK6bBzJslkCgYEAxVE23OQvkCeOV8hJNPZGpJ1mDS+TiOow +oOScMZmZpail181eYbAfMsCr7ri812lSj98NvA2GNVLpddil6LtS1cQ5p36lFBtV +xQUMZiFz4qVbEak+izL+vPaev/mXXsOcibAIQ+qI/0txFpNhJjpaaSy6vRCBYFmc +8pgSoBnBI0ECgYAUKCn2atnpp5aWSTLYgNosBU4vDA1PShD14dnJMaqyr0aZtPhF +v/8b3btFJoGgPMLxgWEZ+2U4ju6sSFhPf7FXvLJu2QfQRkHZRDbEh7t5DLpTK4Fp +va9vl6Ml7uM/HsGpOLuqfIQJUs87OFCc7iCSvMJDDU37I7ekT2GKkpfbCQKBgBrE +0NeY0WcSJrp7/oqD2sOcYurpCG/rrZs2SIZmGzUhMxaa0vIXzbO59dlWELB8pmnE +Tf20K//x9qA5OxDe0PcVPukdQlH+/1zSOYNliG44FqnHtyd1TJ/gKVtMBiAiE4uO +aSClod5Yosf4SJbCFd/s5Iyfv52NqsAyp1w3Aj/BAoGAVCnEiGUfyHlIR+UH4zZW +GXJMeqdZLfcEIszMxLePkml4gUQhoq9oIs/Kw+L1DDxUwzkXN4BNTlFbOSu9gzK1 +dhuIUGfS6RPL88U+ivC3A0y2jT43oUMqe3hiRt360UQ1GXzp2dMnR9odSRB1wHoO +IOjEBZ8341/c9ZHc5PCGAG8= +-----END PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +MIID7zCCAtegAwIBAgIJAIrEIthCfxUCMA0GCSqGSIb3DQEBCwUAMIGNMQswCQYD +VQQGEwJFUzEPMA0GA1UECAwGTWFkcmlkMREwDwYDVQQHDAhBbGNvcmNvbjEMMAoG +A1UECgwDVURTMQ4wDAYDVQQLDAVBY3RvcjESMBAGA1UEAwwJVURTIEFjdG9yMSgw +JgYJKoZIhvcNAQkBFhlzdXBwb3J0QHVkc2VudGVycHJpc2UuY29tMB4XDTE0MTAy +NjIzNDEyNFoXDTI0MTAyMzIzNDEyNFowgY0xCzAJBgNVBAYTAkVTMQ8wDQYDVQQI +DAZNYWRyaWQxETAPBgNVBAcMCEFsY29yY29uMQwwCgYDVQQKDANVRFMxDjAMBgNV +BAsMBUFjdG9yMRIwEAYDVQQDDAlVRFMgQWN0b3IxKDAmBgkqhkiG9w0BCQEWGXN1 +cHBvcnRAdWRzZW50ZXJwcmlzZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQCb50K3mIznNklzyVAD7xSQOSJQ6+NPXj7U9/4zLZ+TvmbQ7RqUUsxb +fxHbeRnoYTWV2nKk4+tHqmvzujLSS/loFhTSMqtrLn7rowSYJoQhKOUkAiQlWkqC +fItWgL5pJopDpNHFul9Rn3dsPMWQTiGeUNR4Y3RnBhr1Q1BsqAzf4m6zFUmgLPPm +VLdF4uJ3Tuz8TSy2gWLs5aSr5do4WamwUfYjRSVMJECmwjUM4rQ8SQgg0sHBeBuD +UGNBvBQFac1G7qUcMReeu8ZrDUtMsXma/l4rA8NB5CRmTrQbTBF4l+jb2BDFebDq +DUK1Oqs9X35yOQfDOAFYHiixPX0IsXOZAgMBAAGjUDBOMB0GA1UdDgQWBBRShS90 +5lJTNvYPIEqP3GxWwG5iiDAfBgNVHSMEGDAWgBRShS905lJTNvYPIEqP3GxWwG5i +iDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAU0Sp4gXhQmRVzq+7+ +vRFUkQuPj4Ga/d9r5Wrbg3hck3+5pwe9/7APoq0P/M0DBhQpiJKjrD6ydUevC+Y/ +43ZOJPhMlNw0o6TdQxOkX6FDwQanLLs7sfvJvqtVzYn3nuRFKT3dvl7Zg44QMw2M +ay42q59fAcpB4LaDx/i7gOYSS5eca3lYW7j7YSr/+ozXK2KlgUkuCUHN95lOq+dF +trmV9mjzM4CNPZqKSE7kpHRywgrXGPCO000NvEGSYf82AtgRSFKiU8NWLQSEPdcB +k//2dsQZw2cRZ8DrC2B6Tb3M+3+CA6wVyqfqZh1SZva3LfGvq/C+u+ItguzPqNpI +xtvM +-----END CERTIFICATE-----''' + with open(certFile, "wt") as f: + f.write(certData) + + return certFile diff --git a/actor/src/udsactor/httpserver.py b/actor/src/udsactor/httpserver.py new file mode 100644 index 000000000..8f46dad59 --- /dev/null +++ b/actor/src/udsactor/httpserver.py @@ -0,0 +1,233 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +''' +@author: Adolfo Gómez, dkmaster at dkmon dot com +''' +from __future__ import unicode_literals + +from udsactor.log import logger +from udsactor import utils +from udsactor.certs import createSelfSignedCert +from udsactor.scriptThread import ScriptExecutorThread + +import threading +import string +import random +import json +import six +from six.moves import socketserver # @UnresolvedImport, pylint: disable=import-error +from six.moves import BaseHTTPServer # @UnresolvedImport, pylint: disable=import-error +import time + +import ssl + +startTime = time.time() + + +class HTTPServerHandler(BaseHTTPServer.BaseHTTPRequestHandler): + protocol_version = 'HTTP/1.0' + server_version = 'UDS Actor Server' + sys_version = '' + + uuid = None + service = None + lock = threading.Lock() + + def sendJsonError(self, code, message): + self.send_response(code) + self.send_header('Content-type', 'application/json') + self.end_headers() + self.wfile.write(json.dumps({'error': message})) + return + + def sendJsonResponse(self, data): + self.send_response(200) + data = json.dumps(data) + self.send_header('Content-type', 'application/json') + self.send_header('Content-Length', len(data)) + self.end_headers() + # Send the html message + self.wfile.write(data) + + def do_GET(self): + # Very simple path & params splitter + path = self.path.split('?')[0][1:].split('/') + try: + params = dict((v.split('=') for v in self.path.split('?')[1].split('&'))) + except Exception: + params = {} + + if path[0] != HTTPServerHandler.uuid: + self.sendJsonError(403, 'Forbidden') + return + + if len(path) != 2: + self.sendJsonResponse("UDS Actor has been running for {} seconds".format(time.time() - startTime)) + return + + try: + operation = getattr(self, 'get_' + path[1]) + result = operation(params) # Protect not POST methods + except AttributeError: + self.sendJsonError(404, 'Method not found') + return + except Exception as e: + logger.error('Got exception executing GET {}: {}'.format(path[1], utils.toUnicode(e.message))) + self.sendJsonError(500, str(e)) + return + + self.sendJsonResponse(result) + + def do_POST(self): + path = self.path.split('?')[0][1:].split('/') + if path[0] != HTTPServerHandler.uuid: + self.sendJsonError(403, 'Forbidden') + return + + if len(path) != 2: + self.sendJsonError(400, 'Invalid request') + return + + try: + HTTPServerHandler.lock.acquire() + length = int(self.headers.get('content-length')) + content = self.rfile.read(length).decode('utf8') + logger.debug('length: {}, content >>{}<<'.format(length, content)) + params = json.loads(content) + + operation = getattr(self, 'post_' + path[1]) + result = operation(params) # Protect not POST methods + except AttributeError: + self.sendJsonError(404, 'Method not found') + return + except Exception as e: + logger.error('Got exception executing POST {}: {}'.format(path[1], utils.toUnicode(e.message))) + self.sendJsonError(500, str(e)) + return + finally: + HTTPServerHandler.lock.release() + + self.sendJsonResponse(result) + + def post_logoff(self, params): + logger.debug('Sending LOGOFF to clients') + HTTPServerHandler.service.ipc.sendLoggofMessage() + return 'ok' + + # Alias + post_logout = post_logoff + + def post_message(self, params): + logger.debug('Sending MESSAGE to clients') + if 'message' not in params: + raise Exception('Invalid message parameters') + HTTPServerHandler.service.ipc.sendMessageMessage(params['message']) + return 'ok' + + def post_script(self, params): + logger.debug('Received script: {}'.format(params)) + if 'script' not in params: + raise Exception('Invalid script parameters') + if 'user' in params: + logger.debug('Sending SCRIPT to clients') + HTTPServerHandler.service.ipc.sendScriptMessage(params['script']) + else: + # Execute script at server space, that is, here + # as a parallel thread + th = ScriptExecutorThread(params['script']) + th.start() + return 'ok' + + def post_preConnect(self, params): + logger.debug('Received Pre connection') + if 'user' not in params or 'protocol' not in params: + raise Exception('Invalid preConnect parameters') + return HTTPServerHandler.service.preConnect(params.get('user'), params.get('protocol')) + + def get_information(self, params): + # TODO: Return something useful? :) + return 'Up and running' + + def get_uuid(self, params): + return self.service.api.uuid + + def log_error(self, fmt, *args): + logger.error('HTTP ' + fmt % args) + + def log_message(self, fmt, *args): + logger.info('HTTP ' + fmt % args) + + +class HTTPServerThread(threading.Thread): + def __init__(self, address, service): + super(self.__class__, self).__init__() + + if HTTPServerHandler.uuid is None: + HTTPServerHandler.uuid = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(48)) + + self.certFile = createSelfSignedCert() + HTTPServerHandler.service = service + + self.initiateServer(address) + + def getPort(self): + return self.address[1] + + def getIp(self): + return self.address[0] + + def initiateServer(self, address): + self.address = (address[0], address[1]) # Copy address & keep it for future reference... + + addr = ('0.0.0.0', address[1]) # Adapt to listen on 0.0.0.0 + + self.server = socketserver.TCPServer(addr, HTTPServerHandler) + self.server.socket = ssl.wrap_socket(self.server.socket, certfile=self.certFile, server_side=True) + + def getServerUrl(self): + return 'https://{}:{}/{}'.format(self.getIp(), self.getPort(), HTTPServerHandler.uuid) + + def stop(self): + logger.debug('Stopping REST Service') + self.server.shutdown() + + def restart(self, address=None): + + if address is None: + # address = self.server.server_address + address = self.address + + self.address = (address[0], self.address[1]) # Copy address & keep it for future reference, port is never changed once assigned on init + + # Listening on 0.0.0.0, does not need to restart listener.. + # self.stop() + # self.initiateServer(address) + + def run(self): + self.server.serve_forever() diff --git a/actor/src/udsactor/ipc.py b/actor/src/udsactor/ipc.py new file mode 100644 index 000000000..fc0ac6c6f --- /dev/null +++ b/actor/src/udsactor/ipc.py @@ -0,0 +1,438 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +''' +@author: Adolfo Gómez, dkmaster at dkmon dot com +''' +from __future__ import unicode_literals + +import socket +import threading +import sys +import traceback +import pickle +import errno +import time +import six + +from udsactor.utils import toUnicode +from udsactor.log import logger + +# The IPC Server will wait for connections from clients +# Clients will open socket, and wait for data from server +# The messages sent (from server) will be the following (subject to future changes): +# Message_id Data Action +# ------------ -------- -------------------------- +# MSG_LOGOFF None Logout user from session +# MSG_MESSAGE message,level Display a message with level (INFO, WARN, ERROR, FATAL) # TODO: Include level, right now only has message +# MSG_SCRIPT python script Execute an specific python script INSIDE CLIENT environment (this messages is not sent right now) +# The messages received (sent from client) will be the following: +# Message_id Data Action +# ------------ -------- -------------------------- +# REQ_LOGOUT Logout user from session +# REQ_INFORMATION None Request information from ipc server (maybe configuration parameters in a near future) +# REQ_LOGIN python script Execute an specific python script INSIDE CLIENT environment (this messages is not sent right now) +# +# All messages are in the form: +# BYTE +# 0 1-2 3 4 ... +# MSG_ID DATA_LENGTH (little endian) Data (can be 0 length) +# With a previos "MAGIC" header in fron of each message + +MSG_LOGOFF = 0xA1 +MSG_MESSAGE = 0xB2 +MSG_SCRIPT = 0xC3 +MSG_INFORMATION = 0xD4 +MSG_TICKET = 0x90 + +# Request messages +REQ_INFORMATION = MSG_INFORMATION +REQ_LOGIN = 0xE5 +REQ_LOGOUT = MSG_LOGOFF +REQ_TICKET = MSG_TICKET +VALID_REQUESTS = (REQ_INFORMATION, REQ_LOGIN, REQ_LOGOUT, REQ_TICKET) + +VALID_MESSAGES = (MSG_LOGOFF, MSG_MESSAGE, MSG_SCRIPT, MSG_INFORMATION) + +REQ_INFORMATION = 0xAA + +# Reverse msgs dict for debugging +REV_DICT = { + MSG_LOGOFF: 'MSG_LOGOFF', + MSG_MESSAGE: 'MSG_MESSAGE', + MSG_SCRIPT: 'MSG_SCRIPT', + MSG_INFORMATION: 'MSG_INFORMATION', + REQ_TICKET: 'REQ_TICKET', + REQ_LOGIN: 'REQ_LOGIN', + REQ_LOGOUT: 'REQ_LOGOUT' +} + +MAGIC = b'\x55\x44\x53\x00' # UDS in hexa with a padded 0 to the right + + +# Allows notifying login/logout from client for linux platform +ALLOW_LOG_METHODS = sys.platform != 'win32' + + +# States for client processor +ST_SECOND_BYTE = 0x01 +ST_RECEIVING = 0x02 +ST_PROCESS_MESSAGE = 0x02 + + +class ClientProcessor(threading.Thread): + def __init__(self, parent, clientSocket): + super(ClientProcessor, self).__init__() + self.parent = parent + self.clientSocket = clientSocket + self.running = False + self.messages = six.moves.queue.Queue(32) # @UndefinedVariable + + def stop(self): + logger.debug('Stoping client processor') + self.running = False + + def processRequest(self, msg, data): + logger.debug('Got Client message {}={}'.format(msg, REV_DICT.get(msg))) + if self.parent.clientMessageProcessor is not None: + self.parent.clientMessageProcessor(msg, data) + + def run(self): + self.running = True + self.clientSocket.setblocking(0) + + state = None + recv_msg = None + recv_data = None + while self.running: + try: + counter = 1024 + while counter > 0: # So we process at least the incoming queue every XX bytes readed + counter -= 1 + b = self.clientSocket.recv(1) + if b == b'': + # Client disconnected + self.running = False + self.processRequest(REQ_LOGOUT, 'CLIENT_CONNECTION_LOST') + break + buf = six.byte2int(b) # Empty buffer, this is set as non-blocking + if state is None: + if buf in VALID_REQUESTS: + logger.debug('State set to {}'.format(buf)) + state = buf + recv_msg = buf + continue # Get next byte + else: + logger.debug('Got unexpected data {}'.format(buf)) + elif state in VALID_REQUESTS: + logger.debug('First length byte is {}'.format(buf)) + msg_len = buf + state = ST_SECOND_BYTE + continue + elif state == ST_SECOND_BYTE: + msg_len += buf << 8 + logger.debug('Second length byte is {}, len is {}'.format(buf, msg_len)) + if msg_len == 0: + self.processRequest(recv_msg, None) + state = None + break + state = ST_RECEIVING + recv_data = b'' + continue + elif state == ST_RECEIVING: + recv_data += six.int2byte(buf) + msg_len -= 1 + if msg_len == 0: + self.processRequest(recv_msg, recv_data) + recv_data = None + state = None + break + else: + logger.debug('Got invalid message from request: {}, state: {}'.format(buf, state)) + except socket.error as e: + # If no data is present, no problem at all, pass to check messages + pass + except Exception as e: + tb = traceback.format_exc() + logger.error('Error: {}, trace: {}'.format(e, tb)) + + if self.running is False: + break + + try: + msg = self.messages.get(block=True, timeout=1) + except six.moves.queue.Empty: # No message got in time @UndefinedVariable + continue + + logger.debug('Got message {}={}'.format(msg, REV_DICT.get(msg))) + + try: + m = msg[1] if msg[1] is not None else b'' + ln = len(m) + data = MAGIC + six.int2byte(msg[0]) + six.int2byte(ln & 0xFF) + six.int2byte(ln >> 8) + m + try: + self.clientSocket.sendall(data) + except socket.error as e: + # Send data error + logger.debug('Socket connection is no more available: {}'.format(e.args)) + self.running = False + except Exception as e: + logger.error('Invalid message in queue: {}'.format(e)) + + logger.debug('Client processor stopped') + try: + self.clientSocket.close() + except Exception: + pass # If can't close, nothing happens, just end thread + + +class ServerIPC(threading.Thread): + + def __init__(self, listenPort, clientMessageProcessor=None): + super(ServerIPC, self).__init__() + self.port = listenPort + self.running = False + self.serverSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.serverSocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.threads = [] + self.clientMessageProcessor = clientMessageProcessor + + def stop(self): + logger.debug('Stopping Server IPC') + self.running = False + for t in self.threads: + t.stop() + socket.socket(socket.AF_INET, socket.SOCK_STREAM).connect(('localhost', self.port)) + self.serverSocket.close() + + for t in self.threads: + t.join() + + def sendMessage(self, msgId, msgData): + ''' + Notify message to all listening threads + ''' + logger.debug('Sending message {}({}),{} to all clients'.format(msgId, REV_DICT.get(msgId), msgData)) + + # Convert to bytes so length is correctly calculated + if isinstance(msgData, six.text_type): + msgData = msgData.encode('utf8') + + for t in self.threads: + if t.isAlive(): + logger.debug('Sending to {}'.format(t)) + t.messages.put((msgId, msgData)) + + def sendLoggofMessage(self): + self.sendMessage(MSG_LOGOFF, '') + + def sendMessageMessage(self, message): + self.sendMessage(MSG_MESSAGE, message) + + def sendScriptMessage(self, script): + self.sendMessage(MSG_SCRIPT, script) + + def sendInformationMessage(self, info): + self.sendMessage(MSG_INFORMATION, pickle.dumps(info)) + + # This one is the only one dumped in json, be care with this!! + def sendTicketMessage(self, ticketData): + self.sendMessage(MSG_TICKET, json.dumps(ticketData)) + + def cleanupFinishedThreads(self): + ''' + Cleans up current threads list + ''' + aliveThreads = [] + for t in self.threads: + if t.isAlive(): + logger.debug('Thread {} is alive'.format(t)) + aliveThreads.append(t) + self.threads[:] = aliveThreads + + def run(self): + self.running = True + + self.serverSocket.bind(('localhost', self.port)) + self.serverSocket.setblocking(1) + self.serverSocket.listen(4) + + while True: + try: + (clientSocket, address) = self.serverSocket.accept() + # Stop processing if thread is mean to stop + if self.running is False: + break + logger.debug('Got connection from {}'.format(address)) + + self.cleanupFinishedThreads() # House keeping + + logger.debug('Starting new thread, current: {}'.format(self.threads)) + t = ClientProcessor(self, clientSocket) + self.threads.append(t) + t.start() + except Exception as e: + logger.error('Got an exception on Server ipc thread: {}'.format(e)) + + +class ClientIPC(threading.Thread): + def __init__(self, listenPort): + super(ClientIPC, self).__init__() + self.port = listenPort + self.running = False + self.clientSocket = None + self.messages = six.moves.queue.Queue(32) # @UndefinedVariable + + self.connect() + + def stop(self): + self.running = False + + def getMessage(self): + while self.running: + try: + return self.messages.get(timeout=1) + except six.moves.queue.Empty: # @UndefinedVariable + continue + + return None + + def sendRequestMessage(self, msg, data=None): + logger.debug('Sending request for msg: {}({}), {}'.format(msg, REV_DICT.get(msg), data)) + if data is None: + data = b'' + + if isinstance(data, six.text_type): # Convert to bytes if necessary + data = data.encode('utf-8') + + ln = len(data) + msg = six.int2byte(msg) + six.int2byte(ln & 0xFF) + six.int2byte(ln >> 8) + data + self.clientSocket.sendall(msg) + + def requestInformation(self): + self.sendRequestMessage(REQ_INFORMATION) + + def sendLogin(self, username): + self.sendRequestMessage(REQ_LOGIN, username) + + def sendLogout(self, username): + self.sendRequestMessage(REQ_LOGOUT, username) + + def requestTicket(self, ticketId, secure=True): + self.sendRequestMessage(REQ_TICKET, json.dumps({'ticketId': ticketId, 'secure': secure})) + + def messageReceived(self): + ''' + Override this method to automatically get notified on new message + received. Message is at self.messages queue + ''' + pass # Messa + + def receiveBytes(self, number): + msg = b'' + while self.running and len(msg) < number: + try: + buf = self.clientSocket.recv(number - len(msg)) + if buf == b'': + logger.debug('Buf {}, msg {}({})'.format(buf, msg, REV_DICT.get(msg))) + self.running = False + break + msg += buf + except socket.timeout: + pass + + if self.running is False: + logger.debug('Not running, returning None') + return None + return msg + + def connect(self): + self.clientSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.clientSocket.connect(('localhost', self.port)) + self.clientSocket.settimeout(2) # 2 seconds timeout + + def run(self): + self.running = True + + while self.running: + try: + msg = b'' + # We look for magic message header + while self.running: # Wait for MAGIC + try: + buf = self.clientSocket.recv(len(MAGIC) - len(msg)) + if buf == b'': + self.running = False + break + msg += buf + if len(msg) != len(MAGIC): + continue # Do not have message + if msg != MAGIC: # Skip first byte an continue searchong + msg = msg[1:] + continue + break + except socket.timeout: # Timeout is here so we can get stop thread + continue + + if self.running is False: + break + + # Now we get message basic data (msg + datalen) + msg = bytearray(self.receiveBytes(3)) + + # We have the magic header, here comes the message itself + if msg is None: + continue + + msgId = msg[0] + dataLen = msg[1] + (msg[2] << 8) + if msgId not in VALID_MESSAGES: + raise Exception('Invalid message id: {}'.format(msgId)) + + data = self.receiveBytes(dataLen) + if data is None: + continue + + self.messages.put((msgId, data)) + self.messageReceived() + + except socket.error as e: + if e.errno == errno.EINTR: + time.sleep(1) # + continue # Ignore interrupted system call + logger.error('Communication with server got an error: {}'.format(toUnicode(e.strerror))) + # self.running = False + return + except Exception as e: + tb = traceback.format_exc() + logger.error('Error: {}, trace: {}'.format(e, tb)) + + try: + self.clientSocket.close() + except Exception: + pass # If can't close, nothing happens, just end thread + diff --git a/actor/src/udsactor/linux/UDSActorService.py b/actor/src/udsactor/linux/UDSActorService.py new file mode 100644 index 000000000..67c6a28c3 --- /dev/null +++ b/actor/src/udsactor/linux/UDSActorService.py @@ -0,0 +1,234 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +''' +@author: Adolfo Gómez, dkmaster at dkmon dot com +''' +from __future__ import unicode_literals + +import sys +import os +import stat +import subprocess + +from udsactor import operations + +from udsactor.service import CommonService +from udsactor.service import initCfg +from udsactor.service import IPC_PORT + +from udsactor import ipc +from udsactor import store +from udsactor.log import logger + +from udsactor.linux.daemon import Daemon +from udsactor.linux import renamer + +POST_CMD = '/etc/udsactor/post' + +try: + from prctl import set_proctitle # @UnresolvedImport +except Exception: # Platform may not include prctl, so in case it's not available, we let the "name" as is + + def set_proctitle(_): + pass + + +class UDSActorSvc(Daemon, CommonService): + rebootMachineAfterOp = False + + def __init__(self, args=None): + Daemon.__init__(self, '/var/run/udsa.pid') + CommonService.__init__(self) + + def rename(self, name, user=None, oldPassword=None, newPassword=None): + ''' + Renames the computer, and optionally sets a password for an user + before this + ''' + hostName = operations.getComputerName() + + if hostName.lower() == name.lower(): + logger.info('Computer name is already {}'.format(hostName)) + self.setReady() + return + + # Check for password change request for an user + if user is not None: + logger.info('Setting password for user {}'.format(user)) + try: + operations.changeUserPassword(user, oldPassword, newPassword) + except Exception as e: + # We stop here without even renaming computer, because the + # process has failed + raise Exception( + 'Could not change password for user {} (maybe invalid current password is configured at broker): {} '.format(user, unicode(e))) + + renamer.rename(name) + + if self.rebootMachineAfterOp is False: + self.setReady() + else: + logger.info('Rebooting computer to activate new name {}'.format(name)) + self.reboot() + + def joinDomain(self, name, domain, ou, account, password): + logger.fatal('Join domain is not supported on linux platforms right now') + + def preConnect(self, user, protocol): + ''' + Invoked when received a PRE Connection request via REST + ''' + # Execute script in /etc/udsactor/post after interacting with broker, if no reboot is requested ofc + # This will be executed only when machine gets "ready" + try: + pre_cmd = store.preApplication() + if os.path.isfile(pre_cmd): + if (os.stat(pre_cmd).st_mode & stat.S_IXUSR) != 0: + subprocess.call([pre_cmd, user, protocol]) + else: + logger.info('PRECONNECT file exists but it it is not executable (needs execution permission by root)') + else: + logger.info('PRECONNECT file not found & not executed') + except Exception: + # Ignore output of execution command + logger.error('Executing preconnect command give') + + return 'ok' + + def run(self): + cfg = initCfg() # Gets a local copy of config to get "reboot" + + logger.debug('CFG: {}'.format(cfg)) + + if cfg is not None: + self.rebootMachineAfterOp = cfg.get('reboot', True) + else: + self.rebootMachineAfterOp = False + + logger.info('Reboot after is {}'.format(self.rebootMachineAfterOp)) + + logger.debug('Running Daemon') + set_proctitle('UDSActorDaemon') + + # Linux daemon will continue running unless something is requested to + while True: + brokerConnected = self.interactWithBroker() + if brokerConnected is False: + logger.debug('Interact with broker returned false, stopping service after a while') + return + elif brokerConnected is True: + break + + # If brokerConnected returns None, repeat the cycle + self.doWait(16000) # Wait for a looong while + + if self.isAlive is False: + logger.debug('The service is not alive after broker interaction, stopping it') + return + + if self.rebootRequested is True: + logger.debug('Reboot has been requested, stopping service') + return + + # Execute script in /etc/udsactor/post after interacting with broker, if no reboot is requested ofc + # This will be executed only when machine gets "ready" + try: + + if os.path.isfile(POST_CMD): + if (os.stat(POST_CMD).st_mode & stat.S_IXUSR) != 0: + subprocess.call([POST_CMD, ]) + else: + logger.info('POST file exists but it it is not executable (needs execution permission by root)') + else: + logger.info('POST file not found & not executed') + except Exception as e: + # Ignore output of execution command + logger.error('Executing post command give') + + self.initIPC() + + # ********************* + # * Main Service loop * + # ********************* + # Counter used to check ip changes only once every 10 seconds, for + # example + counter = 0 + while self.isAlive: + counter += 1 + if counter % 10 == 0: + self.checkIpsChanged() + # In milliseconds, will break + self.doWait(1000) + + self.endIPC() + self.endAPI() + + self.notifyStop() + + +def usage(): + sys.stderr.write("usage: {} start|stop|restart|login 'username'|logout 'username'\n".format(sys.argv[0])) + sys.exit(2) + + +if __name__ == '__main__': + logger.setLevel(20000) + + if len(sys.argv) == 3 and sys.argv[1] in ('login', 'logout'): + logger.debug('Running client udsactor') + client = None + try: + client = ipc.ClientIPC(IPC_PORT) + if 'login' == sys.argv[1]: + client.sendLogin(sys.argv[2]) + sys.exit(0) + elif 'logout' == sys.argv[1]: + client.sendLogout(sys.argv[2]) + sys.exit(0) + else: + usage() + except Exception as e: + logger.error(e) + elif len(sys.argv) != 2: + usage() + + logger.debug('Executing actor') + daemon = UDSActorSvc() + if len(sys.argv) == 2: + if 'start' == sys.argv[1]: + daemon.start() + elif 'stop' == sys.argv[1]: + daemon.stop() + elif 'restart' == sys.argv[1]: + daemon.restart() + else: + usage() + sys.exit(0) + else: + usage() diff --git a/actor/src/udsactor/linux/__init__.py b/actor/src/udsactor/linux/__init__.py new file mode 100644 index 000000000..3a98c7807 --- /dev/null +++ b/actor/src/udsactor/linux/__init__.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +''' +@author: Adolfo Gómez, dkmaster at dkmon dot com +''' +from __future__ import unicode_literals diff --git a/actor/src/udsactor/linux/daemon.py b/actor/src/udsactor/linux/daemon.py new file mode 100644 index 000000000..1953b62a6 --- /dev/null +++ b/actor/src/udsactor/linux/daemon.py @@ -0,0 +1,183 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014-2018 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +''' +@author: : http://www.jejik.com/authors/sander_marechal/ +@see: : http://www.jejik.com/articles/2007/02/a_simple_unix_linux_daemon_in_python/ +''' + +from __future__ import unicode_literals +import sys +import os +import time +import atexit +from udsactor.log import logger + +from signal import SIGTERM + + +class Daemon: + """ + A generic daemon class. + + Usage: subclass the Daemon class and override the run() method + """ + + def __init__(self, pidfile, stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'): + self.stdin = stdin + self.stdout = stdout + self.stderr = stderr + self.pidfile = pidfile + + def daemonize(self): + """ + do the UNIX double-fork magic, see Stevens' "Advanced + Programming in the UNIX Environment" for details (ISBN 0201563177) + http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16 + """ + try: + pid = os.fork() + if pid > 0: + # exit first parent + sys.exit(0) + except OSError as e: + logger.error("fork #1 error: {}".format(e)) + sys.stderr.write("fork #1 failed: {}\n".format(e)) + sys.exit(1) + + # decouple from parent environment + os.chdir("/") + os.setsid() + os.umask(0) + + # do second fork + try: + pid = os.fork() + if pid > 0: + # exit from second parent + sys.exit(0) + except OSError as e: + logger.error("fork #2 error: {}".format(e)) + sys.stderr.write("fork #2 failed: {}\n".format(e)) + sys.exit(1) + + # redirect standard file descriptors + sys.stdout.flush() + sys.stderr.flush() + si = open(self.stdin, 'r') + so = open(self.stdout, 'ab+') + se = open(self.stderr, 'ab+', 0) + os.dup2(si.fileno(), sys.stdin.fileno()) + os.dup2(so.fileno(), sys.stdout.fileno()) + os.dup2(se.fileno(), sys.stderr.fileno()) + + # write pidfile + atexit.register(self.delpid) + pid = str(os.getpid()) + with open(self.pidfile, 'w+') as f: + f.write("{}\n".format(pid)) + + def delpid(self): + try: + os.remove(self.pidfile) + except Exception: + # Not found/not permissions or whatever... + pass + + def start(self): + """ + Start the daemon + """ + logger.debug('Starting daemon') + # Check for a pidfile to see if the daemon already runs + try: + pf = open(self.pidfile, 'r') + pid = int(pf.read().strip()) + pf.close() + except IOError: + pid = None + + if pid: + message = "pidfile {} already exist. Daemon already running?\n".format(pid) + logger.error(message) + sys.stderr.write(message) + sys.exit(1) + + # Start the daemon + self.daemonize() + try: + self.run() + except Exception as e: + logger.error('Exception running process: {}'.format(e)) + + if os.path.exists(self.pidfile): + os.remove(self.pidfile) + + def stop(self): + """ + Stop the daemon + """ + # Get the pid from the pidfile + try: + pf = open(self.pidfile, 'r') + pid = int(pf.read().strip()) + pf.close() + except IOError: + pid = None + + if pid is None: + message = "pidfile {} does not exist. Daemon not running?\n".format(self.pidfile) + logger.info(message) + # sys.stderr.write(message) + return # not an error in a restart + + # Try killing the daemon process + try: + while True: + os.kill(pid, SIGTERM) + time.sleep(1) + except OSError as err: + if err.errno == 3: # No such process + if os.path.exists(self.pidfile): + os.remove(self.pidfile) + else: + sys.stderr.write(err) + sys.exit(1) + + def restart(self): + """ + Restart the daemon + """ + self.stop() + self.start() + + # Overridables + def run(self): + """ + You should override this method when you subclass Daemon. It will be called after the process has been + daemonized by start() or restart(). + """ diff --git a/actor/src/udsactor/linux/log.py b/actor/src/udsactor/linux/log.py new file mode 100644 index 000000000..542ef9bff --- /dev/null +++ b/actor/src/udsactor/linux/log.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +''' +@author: Adolfo Gómez, dkmaster at dkmon dot com +''' +from __future__ import unicode_literals + +import logging +import os +import tempfile +import six + +# Valid logging levels, from UDS Broker (uds.core.utils.log) +OTHER, DEBUG, INFO, WARN, ERROR, FATAL = (10000 * (x + 1) for x in six.moves.xrange(6)) # @UndefinedVariable + + +class LocalLogger(object): + def __init__(self): + # tempdir is different for "user application" and "service" + # service wil get c:\windows\temp, while user will get c:\users\XXX\temp + # Try to open logger at /var/log path + # If it fails (access denied normally), will try to open one at user's home folder, and if + # agaim it fails, open it at the tmpPath + + for logDir in ('/var/log', os.path.expanduser('~'), tempfile.gettempdir()): + try: + fname = os.path.join(logDir, 'udsactor.log') + logging.basicConfig( + filename=fname, + filemode='a', + format='%(levelname)s %(asctime)s %(message)s', + level=logging.DEBUG + ) + self.logger = logging.getLogger('udsactor') + os.chmod(fname, 0o0600) + return + except Exception: + pass + + # Logger can't be set + self.logger = None + + def log(self, level, message): + # Debug messages are logged to a file + # our loglevels are 10000 (other), 20000 (debug), .... + # logging levels are 10 (debug), 20 (info) + # OTHER = logging.NOTSET + self.logger.log(int(level / 1000) - 10, message) + + def isWindows(self): + return False + + def isLinux(self): + return True diff --git a/actor/src/udsactor/linux/operations.py b/actor/src/udsactor/linux/operations.py new file mode 100644 index 000000000..e29127d54 --- /dev/null +++ b/actor/src/udsactor/linux/operations.py @@ -0,0 +1,248 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +''' +@author: Adolfo Gómez, dkmaster at dkmon dot com +''' +from __future__ import unicode_literals + +import socket +import platform +import fcntl +import os +import ctypes # @UnusedImport +import ctypes.util +import subprocess +import struct +import array +import six +from udsactor import utils +from .renamer import rename + + +def _getMacAddr(ifname): + ''' + Returns the mac address of an interface + Mac is returned as unicode utf-8 encoded + ''' + if isinstance(ifname, list): + return dict([(name, _getMacAddr(name)) for name in ifname]) + if isinstance(ifname, six.text_type): + ifname = ifname.encode('utf-8') # If unicode, convert to bytes (or str in python 2.7) + try: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + info = bytearray(fcntl.ioctl(s.fileno(), 0x8927, struct.pack(str('256s'), ifname[:15]))) + return six.text_type(''.join(['%02x:' % char for char in info[18:24]])[:-1]).upper() + except Exception: + return None + + +def _getIpAddr(ifname): + ''' + Returns the ip address of an interface + Ip is returned as unicode utf-8 encoded + ''' + if isinstance(ifname, list): + return dict([(name, _getIpAddr(name)) for name in ifname]) + if isinstance(ifname, six.text_type): + ifname = ifname.encode('utf-8') # If unicode, convert to bytes (or str in python 2.7) + try: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + return six.text_type(socket.inet_ntoa(fcntl.ioctl( + s.fileno(), + 0x8915, # SIOCGIFADDR + struct.pack(str('256s'), ifname[:15]) + )[20:24])) + except Exception: + return None + + +def _getInterfaces(): + ''' + Returns a list of interfaces names coded in utf-8 + ''' + max_possible = 128 # arbitrary. raise if needed. + space = max_possible * 16 + if platform.architecture()[0] == '32bit': + offset, length = 32, 32 + elif platform.architecture()[0] == '64bit': + offset, length = 16, 40 + else: + raise OSError('Unknown arquitecture {0}'.format(platform.architecture()[0])) + + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + names = array.array(str('B'), b'\0' * space) + outbytes = struct.unpack(str('iL'), fcntl.ioctl( + s.fileno(), + 0x8912, # SIOCGIFCONF + struct.pack(str('iL'), space, names.buffer_info()[0]) + ))[0] + namestr = names.tostring() + # return namestr, outbytes + return [namestr[i:i + offset].split(b'\0', 1)[0].decode('utf-8') for i in range(0, outbytes, length)] + + +def _getIpAndMac(ifname): + ip, mac = _getIpAddr(ifname), _getMacAddr(ifname) + return (ip, mac) + + +def getComputerName(): + ''' + Returns computer name, with no domain + ''' + return socket.gethostname().split('.')[0] + + +def getNetworkInfo(): + for ifname in _getInterfaces(): + ip, mac = _getIpAndMac(ifname) + if mac != '00:00:00:00:00:00' and ip.startswith('169.254') is False: # Skips local interfaces & interfaces with no dhcp IPs + yield utils.Bunch(name=ifname, mac=mac, ip=ip) + + +def getDomainName(): + return '' + + +def getLinuxVersion(): + lv = platform.linux_distribution() + return lv[0] + ', ' + lv[1] + + +def reboot(flags=0): + ''' + Simple reboot using os command + ''' + # Workaround for dummy thread + if six.PY3 is False: + import threading + threading._DummyThread._Thread__stop = lambda x: 42 + + subprocess.call(['/sbin/shutdown', 'now', '-r']) + + +def loggoff(): + ''' + Right now restarts the machine... + ''' + # Workaround for dummy thread + if six.PY3 is False: + import threading + threading._DummyThread._Thread__stop = lambda x: 42 + + subprocess.call(['/usr/bin/pkill', '-u', os.environ['USER']]) + # subprocess.call(['/sbin/shutdown', 'now', '-r']) + # subprocess.call(['/usr/bin/systemctl', 'reboot', '-i']) + + +def renameComputer(newName): + rename(newName) + + +def joinDomain(domain, ou, account, password, executeInOneStep=False): + pass + + +def changeUserPassword(user, oldPassword, newPassword): + ''' + Simple password change for user using command line + ''' + os.system('echo "{1}\n{1}" | /usr/bin/passwd {0} 2> /dev/null'.format(user, newPassword)) + + +class XScreenSaverInfo(ctypes.Structure): + _fields_ = [('window', ctypes.c_long), + ('state', ctypes.c_int), + ('kind', ctypes.c_int), + ('til_or_since', ctypes.c_ulong), + ('idle', ctypes.c_ulong), + ('eventMask', ctypes.c_ulong)] + +# Initialize xlib & xss +try: + xlibPath = ctypes.util.find_library('X11') + xssPath = ctypes.util.find_library('Xss') + xlib = xss = None + if not xlibPath or not xssPath: + raise Exception() + xlib = ctypes.cdll.LoadLibrary(xlibPath) + xss = ctypes.cdll.LoadLibrary(xssPath) + + # Fix result type to XScreenSaverInfo Structure + xss.XScreenSaverQueryExtension.restype = ctypes.c_int + xss.XScreenSaverAllocInfo.restype = ctypes.POINTER(XScreenSaverInfo) # Result in a XScreenSaverInfo structure + display = xlib.XOpenDisplay(None) + info = xss.XScreenSaverAllocInfo() +except Exception: # Libraries not accesible, not found or whatever.. + xlib = xss = display = info = None + + +def initIdleDuration(atLeastSeconds): + ''' + On linux we set the screensaver to at least required seconds, or we never will get "idle" + ''' + # Workaround for dummy thread + if six.PY3 is False: + import threading + threading._DummyThread._Thread__stop = lambda x: 42 + + subprocess.call(['/usr/bin/xset', 's', '{}'.format(atLeastSeconds + 30)]) + # And now reset it + subprocess.call(['/usr/bin/xset', 's', 'reset']) + + +def getIdleDuration(): + ''' + Returns idle duration, in seconds + ''' + if xlib is None or xss is None: + return 0 # Libraries not available + + event_base = ctypes.c_int() + error_base = ctypes.c_int() + + available = xss.XScreenSaverQueryExtension(display, ctypes.byref(event_base), ctypes.byref(error_base)) + + if available != 1: + return 0 # No screen saver is available, no way of getting idle + + xss.XScreenSaverQueryInfo(display, xlib.XDefaultRootWindow(display), info) + + # Centos seems to set state to 1?? (weird, but it's happening don't know why... will try this way) + if info.contents.state != 0 and 'centos' not in platform.linux_distribution()[0].lower().strip(): + return 3600 * 100 * 1000 # If screen saver is active, return a high enough value + + return info.contents.idle / 1000.0 + + +def getCurrentUser(): + ''' + Returns current logged in user + ''' + return os.environ['USER'] diff --git a/actor/src/udsactor/linux/renamer/__init__.py b/actor/src/udsactor/linux/renamer/__init__.py new file mode 100644 index 000000000..54cd3c06f --- /dev/null +++ b/actor/src/udsactor/linux/renamer/__init__.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +''' +@author: Adolfo Gómez, dkmaster at dkmon dot com +''' +from __future__ import unicode_literals + +import platform +import os +import sys +import pkgutil + +from udsactor.log import logger + +renamers = {} + + +# Renamers now are for IPv4 only addresses +def rename(newName): + distribution = platform.linux_distribution()[0].lower().strip() + if distribution in renamers: + return renamers[distribution](newName) + + # Try Debian renamer, simplest one + logger.info('Renamer for platform "{0}" not found, tryin debian renamer'.format(distribution)) + return renamers['debian'](newName) + + +# Do load of packages +def _init(): + pkgpath = os.path.dirname(sys.modules[__name__].__file__) + for _, name, _ in pkgutil.iter_modules([pkgpath]): + __import__(__name__ + '.' + name, globals(), locals()) + +_init() \ No newline at end of file diff --git a/actor/src/udsactor/linux/renamer/debian.py b/actor/src/udsactor/linux/renamer/debian.py new file mode 100644 index 000000000..c1761656c --- /dev/null +++ b/actor/src/udsactor/linux/renamer/debian.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +''' +@author: Adolfo Gómez, dkmaster at dkmon dot com +''' +from __future__ import unicode_literals + +from udsactor.linux.renamer import renamers +from udsactor.log import logger + +import os + + +def rename(newName): + ''' + Debian renamer + Expects new host name on newName + Host does not needs to be rebooted after renaming + ''' + logger.debug('using Debian renamer') + + with open('/etc/hostname', 'w') as hostname: + hostname.write(newName) + + # Force system new name + os.system('/bin/hostname %s' % newName) + + # add name to "hosts" + with open('/etc/hosts', 'r') as hosts: + lines = hosts.readlines() + with open('/etc/hosts', 'w') as hosts: + hosts.write("127.0.1.1\t%s\n" % newName) + for l in lines: + if l[:9] == '127.0.1.1': # Skips existing 127.0.1.1. if it already exists + continue + hosts.write(l) + + return True + +# All names in lower case +renamers['debian'] = rename +renamers['ubuntu'] = rename diff --git a/actor/src/udsactor/linux/renamer/opensuse.py b/actor/src/udsactor/linux/renamer/opensuse.py new file mode 100644 index 000000000..d7826e009 --- /dev/null +++ b/actor/src/udsactor/linux/renamer/opensuse.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +''' +@author: Adolfo Gómez, dkmaster at dkmon dot com +''' +from __future__ import unicode_literals + +from udsactor.linux.renamer import renamers +from udsactor.log import logger + +import os + + +def rename(newName): + ''' + RH, Centos, Fedora Renamer + Expects new host name on newName + Host does not needs to be rebooted after renaming + ''' + logger.debug('using SUSE renamer') + + with open('/etc/hostname', 'w') as hostname: + hostname.write(newName) + + # Force system new name + os.system('/bin/hostname %s' % newName) + + # add name to "hosts" + with open('/etc/hosts', 'r') as hosts: + lines = hosts.readlines() + with open('/etc/hosts', 'w') as hosts: + hosts.write("127.0.1.1\t{}\n".format(newName)) + for l in lines: + if l[:9] != '127.0.1.1': # Skips existing 127.0.1.1. if it already exists + hosts.write(l) + + return True + +# All names in lower case +renamers['opensuse'] = rename +renamers['suse'] = rename diff --git a/actor/src/udsactor/linux/renamer/redhat.py b/actor/src/udsactor/linux/renamer/redhat.py new file mode 100644 index 000000000..76a0dc9a2 --- /dev/null +++ b/actor/src/udsactor/linux/renamer/redhat.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +''' +@author: Adolfo Gómez, dkmaster at dkmon dot com +''' +from __future__ import unicode_literals + +from udsactor.linux.renamer import renamers +from udsactor.log import logger + +import os + + +def rename(newName): + ''' + RH, Centos, Fedora Renamer + Expects new host name on newName + Host does not needs to be rebooted after renaming + ''' + logger.debug('using RH renamer') + + with open('/etc/hostname', 'w') as hostname: + hostname.write(newName) + + # Force system new name + os.system('/bin/hostname %s' % newName) + + # add name to "hosts" + with open('/etc/hosts', 'r') as hosts: + lines = hosts.readlines() + with open('/etc/hosts', 'w') as hosts: + hosts.write("127.0.1.1\t{}\n".format(newName)) + for l in lines: + if l[:9] != '127.0.1.1': # Skips existing 127.0.1.1. if it already exists + hosts.write(l) + + with open('/etc/sysconfig/network', 'r') as net: + lines = net.readlines() + with open('/etc/sysconfig/network', 'w') as net: + net.write('HOSTNAME={}\n'.format(newName)) + for l in lines: + if l[:8] != 'HOSTNAME': + net.write(l) + + return True + +# All names in lower case +renamers['centos linux'] = rename +renamers['centos'] = rename +renamers['fedora'] = rename diff --git a/actor/src/udsactor/linux/store.py b/actor/src/udsactor/linux/store.py new file mode 100644 index 000000000..29e46e956 --- /dev/null +++ b/actor/src/udsactor/linux/store.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +''' +@author: Adolfo Gómez, dkmaster at dkmon dot com +''' + +import six +import os + +DEBUG = False + +CONFIGFILE = '/etc/udsactor/udsactor.cfg' if DEBUG is False else '/tmp/udsactor.cfg' +PRECONNECT_CMD = '/etc/udsactor/pre' + + +def checkPermissions(): + return True if DEBUG else os.getuid() == 0 + + +def readConfig(): + res = {} + try: + cfg = six.moves.configparser.SafeConfigParser() # @UndefinedVariable + cfg.optionxform = six.text_type + cfg.read(CONFIGFILE) + # Just reads 'uds' section + for key in cfg.options('uds'): + res[key] = cfg.get('uds', key) + if res[key].lower() in ('true', 'yes', 'si'): + res[key] = True + elif res[key].lower() in ('false', 'no'): + res[key] = False + except Exception: + pass + + return res + + +def writeConfig(data): + cfg = six.moves.configparser.SafeConfigParser() # @UndefinedVariable + cfg.optionxform = six.text_type + cfg.add_section('uds') + for key, val in data.items(): + cfg.set('uds', key, str(val)) + + # Ensures exists destination folder + dirname = os.path.dirname(CONFIGFILE) + if not os.path.exists(dirname): + os.mkdir(dirname, mode=0o700) # Will create only if route to path already exists, for example, /etc (that must... :-)) + + with open(CONFIGFILE, 'w') as f: + cfg.write(f) + + os.chmod(CONFIGFILE, 0o0600) + + +def useOldJoinSystem(): + return False + + +# Right now, we do not really need an application to be run on "startup" as could ocur with windows +def runApplication(): + return None + + +def preApplication(): + return PRECONNECT_CMD diff --git a/actor/src/udsactor/log.py b/actor/src/udsactor/log.py new file mode 100644 index 000000000..55e6c9fba --- /dev/null +++ b/actor/src/udsactor/log.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +''' +@author: Adolfo Gómez, dkmaster at dkmon dot com +''' +from __future__ import unicode_literals + +import traceback +import sys +import six + +if sys.platform == 'win32': + from udsactor.windows.log import LocalLogger # @UnusedImport +else: + from udsactor.linux.log import LocalLogger # @Reimport + +# Valid logging levels, from UDS Broker (uds.core.utils.log) +OTHER, DEBUG, INFO, WARN, ERROR, FATAL = (10000 * (x + 1) for x in six.moves.xrange(6)) # @UndefinedVariable + + +class Logger(object): + + def __init__(self): + self.logLevel = INFO + self.logger = LocalLogger() + self.remoteLogger = None + + def setLevel(self, level): + ''' + Sets log level filter (minimum level required for a log message to be processed) + :param level: Any message with a level below this will be filtered out + ''' + self.logLevel = int(level) # Ensures level is an integer or fails + + def setRemoteLogger(self, remoteLogger): + self.remoteLogger = remoteLogger + + def log(self, level, message): + if level < self.logLevel: # Skip not wanted messages + return + + # If remote logger is available, notify message to it + try: + if self.remoteLogger is not None and self.remoteLogger.isConnected and level >= INFO: + self.remoteLogger.log(level, message) + except Exception as e: + self.logger.log(FATAL, 'Error notifying log to broker: {}'.format(e.message)) + + self.logger.log(level, message) + + def debug(self, message): + self.log(DEBUG, message) + + def warn(self, message): + self.log(WARN, message) + + def info(self, message): + self.log(INFO, message) + + def error(self, message): + self.log(ERROR, message) + + def fatal(self, message): + self.log(FATAL, message) + + def exception(self): + try: + tb = traceback.format_exc() + except Exception: + tb = '(could not get traceback!)' + + self.log(DEBUG, tb) + + def flush(self): + pass + + +logger = Logger() diff --git a/actor/src/udsactor/operations.py b/actor/src/udsactor/operations.py new file mode 100644 index 000000000..fc241ab48 --- /dev/null +++ b/actor/src/udsactor/operations.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +''' +@author: Adolfo Gómez, dkmaster at dkmon dot com +''' +# pylint: disable=unused-wildcard-import,wildcard-import + +from __future__ import unicode_literals + +import sys +if sys.platform == 'win32': + from .windows.operations import * # @UnusedWildImport +else: + from .linux.operations import * # @UnusedWildImport diff --git a/actor/src/udsactor/rest.py b/actor/src/udsactor/rest.py new file mode 100644 index 000000000..e05e8786b --- /dev/null +++ b/actor/src/udsactor/rest.py @@ -0,0 +1,231 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2019 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +''' +@author: Adolfo Gómez, dkmaster at dkmon dot com +''' +# pylint: disable=invalid-name +import uuid +import warnings +import json +import logging +import typing + +import requests + +from udsactor import VERSION + +from .utils import exceptionToMessage +from .log import logger + +class RESTError(Exception): + ERRCODE = 0 + +class RESTConnectionError(RESTError): + ERRCODE = -1 + +# Errors ""raised"" from broker +class RESTInvalidKeyError(RESTError): + ERRCODE = 1 + +class RESTUnmanagedHostError(RESTError): + ERRCODE = 2 + +class RESTUserServiceNotFoundError(RESTError): + ERRCODE = 3 + +class RESTOsManagerError(RESTError): + ERRCODE = 4 + + +# Disable warnings log messages +try: + import urllib3 # @UnusedImport @UnresolvedImport +except Exception: + from requests.packages import urllib3 # @Reimport @UnresolvedImport + +try: + urllib3.disable_warnings() # @UndefinedVariable + warnings.simplefilter("ignore") +except Exception: + pass # In fact, isn't too important, but will log warns to logging file + + +def ensureResultIsOk(result: typing.Any) -> None: + if 'error' not in result: + return + + for i in (RESTInvalidKeyError, RESTUnmanagedHostError, RESTUserServiceNotFoundError, RESTOsManagerError): + if result['error'] == i.ERRCODE: + raise i(result['result']) + + err = RESTError(result['result']) + err.ERRCODE = result['error'] + raise err + + +class REST: + def __init__(self, host: str, validateCert: bool) -> None: + self.host = host + self.validateCert = validateCert + self.url = "{}://{}/rest/actor/".format(('https', 'https'), self.host) + # Disable logging requests messages except for errors, ... + logging.getLogger("requests").setLevel(logging.CRITICAL) + # Tries to disable all warnings + try: + warnings.simplefilter("ignore") # Disables all warnings + except Exception: + pass + + def _getUrl(self, method, key=None, ids=None): + url = self.url + method + params = [] + if key is not None: + params.append('key=' + key) + if ids is not None: + params.append('id=' + ids) + params.append('version=' + VERSION) + + if len(params) > 0: + url += '?' + '&'.join(params) + + return url + + def _request(self, url, data=None): + try: + if data is None: + # Old requests version does not support verify, but they do not checks ssl certificate by default + if self.newerRequestLib: + r = requests.get(url, verify=VERIFY_CERT) + else: + logger.debug('Requesting with old') + r = requests.get(url) # Always ignore certs?? + else: + if data == '': + data = '{"dummy": true}' # Ensures no proxy rewrites POST as GET because body is empty... + if self.newerRequestLib: + r = requests.post(url, data=data, headers={'content-type': 'application/json'}, verify=VERIFY_CERT) + else: + logger.debug('Requesting with old') + r = requests.post(url, data=data, headers={'content-type': 'application/json'}) + + # From versions of requests, content maybe bytes or str. We need str for json.loads + content = r.content + if not isinstance(content, six.text_type): + content = content.decode('utf8') + r = json.loads(content) # Using instead of r.json() to make compatible with oooold rquests lib versions + except requests.exceptions.RequestException as e: + raise ConnectionError(e) + except Exception as e: + raise ConnectionError(exceptionToMessage(e)) + + ensureResultIsOk(r) + + return r + + @property + def isConnected(self): + return self.uuid is not None + + def test(self): + url = self._getUrl('test', self.masterKey) + return self._request(url)['result'] + + def init(self, ids): + ''' + Ids is a comma separated values indicating MAC=ip + Server returns: + uuid, mac + Optionally can return an third parameter, that is max "idle" request time + ''' + logger.debug('Invoking init') + url = self._getUrl('init', key=self.masterKey, ids=ids) + res = self._request(url)['result'] + logger.debug('Got response parameters: {}'.format(res)) + self.uuid, self.mac = res[0:2] + # Optional idle parameter + try: + self.idle = int(res[2]) + if self.idle < 30: + self.idle = None # No values under 30 seconds are allowed :) + except Exception: + self.idle = None + + return self.uuid + + def postMessage(self, msg, data, processData=True): + logger.debug('Invoking post message {} with data {}'.format(msg, data)) + + if self.uuid is None: + raise ConnectionError('REST api has not been initialized') + + if processData: + if data and not isinstance(data, six.text_type): + data = data.decode('utf8') + data = json.dumps({'data': data}) + url = self._getUrl('/'.join([self.uuid, msg])) + return self._request(url, data)['result'] + + def notifyComm(self, url): + logger.debug('Notifying comms {}'.format(url)) + return self.postMessage('notifyComms', url) + + def login(self, username): + logger.debug('Notifying login {}'.format(username)) + return self.postMessage('login', username) + + def logout(self, username): + logger.debug('Notifying logout {}'.format(username)) + return self.postMessage('logout', username) + + def information(self): + logger.debug('Requesting information'.format()) + return self.postMessage('information', '') + + def setReady(self, ipsInfo, hostName=None): + logger.debug('Notifying readyness: {}'.format(ipsInfo)) + # data = ','.join(['{}={}'.format(v[0], v[1]) for v in ipsInfo]) + data = { + 'ips': ipsInfo, + 'hostname': hostName + } + return self.postMessage('ready', data) + + def notifyIpChanges(self, ipsInfo): + logger.debug('Notifying ip changes: {}'.format(ipsInfo)) + data = ','.join(['{}={}'.format(v[0], v[1]) for v in ipsInfo]) + return self.postMessage('ip', data) + + def getTicket(self, ticketId, secure=False): + url = self._getUrl('ticket/' + ticketId, self.masterKey) + "&secure={}".format('1' if secure else '0') + return self._request(url)['result'] + + + def log(self, logLevel, message): + data = json.dumps({'message': message, 'level': logLevel}) + return self.postMessage('log', data, processData=False) + diff --git a/actor/src/udsactor/scriptThread.py b/actor/src/udsactor/scriptThread.py new file mode 100644 index 000000000..14b1bcfc6 --- /dev/null +++ b/actor/src/udsactor/scriptThread.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 201 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +''' +@author: Adolfo Gómez, dkmaster at dkmon dot com +''' + +# pylint: disable-msg=E1101,W0703 + +from udsactor.log import logger + +import threading +import six + + +class ScriptExecutorThread(threading.Thread): + def __init__(self, script): + super(ScriptExecutorThread, self).__init__() + self.script = script + + def run(self): + try: + logger.debug('Executing script: {}'.format(self.script)) + six.exec_(self.script, globals(), None) + except Exception as e: + logger.error('Error executing script: {}'.format(e)) diff --git a/actor/src/udsactor/service.py b/actor/src/udsactor/service.py new file mode 100644 index 000000000..d55b1620a --- /dev/null +++ b/actor/src/udsactor/service.py @@ -0,0 +1,377 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +''' +@author: Adolfo Gómez, dkmaster at dkmon dot com +''' +from __future__ import unicode_literals + +from udsactor.log import logger + +from . import operations +from . import store +from . import REST +from . import ipc +from . import httpserver +from .scriptThread import ScriptExecutorThread +from .utils import exceptionToMessage + +import socket +import time +import random +import os +import subprocess +import shlex +import stat +import json + +IPC_PORT = 39188 + +cfg = None + + +def initCfg(): + global cfg # pylint: disable=global-statement + cfg = store.readConfig() + + if logger.logger.isWindows(): + # Logs will also go to windows event log for services + logger.logger.serviceLogger = True + + if cfg is not None: + logger.setLevel(cfg.get('logLevel', 20000)) + else: + logger.setLevel(20000) + cfg = {} + + # If ANY var is missing, reset cfg + for v in ('host', 'ssl', 'masterKey'): + if v not in cfg: + cfg = None + break + + return cfg + + +class CommonService(object): + + def __init__(self): + self.isAlive = True + self.api = None + self.ipc = None + self.httpServer = None + self.rebootRequested = False + self.knownIps = [] + self.loggedIn = False + socket.setdefaulttimeout(20) + + def reboot(self): + self.rebootRequested = True + + def execute(self, cmdLine, section): # pylint: disable=no-self-use + cmd = shlex.split(cmdLine, posix=False) + + if os.path.isfile(cmd[0]): + if (os.stat(cmd[0]).st_mode & stat.S_IXUSR) != 0: + try: + res = subprocess.check_call(cmd) + except Exception as e: + logger.error('Got exception executing: {} - {}'.format(cmdLine, e)) + return False + logger.info('Result of executing cmd was {}'.format(res)) + return True + else: + logger.error('{} file exists but it it is not executable (needs execution permission by admin/root)'.format(section)) + else: + logger.error('{} file not found & not executed'.format(section)) + + return False + + def setReady(self): + self.api.setReady([(v.mac, v.ip) for v in operations.getNetworkInfo()]) + + def interactWithBroker(self): + ''' + Returns True to continue to main loop, false to stop & exit service + ''' + # If no configuration is found, stop service + if cfg is None: + logger.fatal('No configuration found, stopping service') + return False + + self.api = REST.Api(cfg['host'], cfg['masterKey'], cfg['ssl']) + + # Wait for Broker to be ready + counter = 0 + while self.isAlive: + try: + # getNetworkInfo is a generator function + netInfo = tuple(operations.getNetworkInfo()) + self.knownIps = dict(((i.mac, i.ip) for i in netInfo)) + ids = ','.join([i.mac for i in netInfo]) + if ids == '': + # Wait for any network interface to be ready + logger.debug('No valid network interfaces found, retrying in a while...') + raise Exception() + logger.debug('Ids: {}'.format(ids)) + self.api.init(ids) + # Set remote logger to notify log info to broker + logger.setRemoteLogger(self.api) + + break + except REST.InvalidKeyError: + logger.fatal('Can\'t sync with broker: Invalid broker Master Key') + return False + except REST.UnmanagedHostError: + # Maybe interface that is registered with broker is not enabled already? + # Right now, we thing that the interface connected to broker is + # the interface that broker will know, let's see how this works + logger.fatal('This host is not managed by UDS Broker (ids: {})'.format(ids)) + return False # On unmanaged hosts, there is no reason right now to continue running + except Exception as e: + logger.debug('Exception on network info: retrying') + # Any other error is expectable and recoverable, so let's wait a bit and retry again + # but, if too many errors, will log it (one every minute, for + # example) + counter += 1 + if counter % 60 == 0: # Every 5 minutes, raise a log + logger.info('Trying to inititialize connection with broker (last error: {})'.format(exceptionToMessage(e))) + # Wait a bit before next check + self.doWait(5000) + + # Now try to run the "runonce" element + runOnce = store.runApplication() + if runOnce is not None: + logger.info('Executing runOnce app: {}'.format(runOnce)) + if self.execute(runOnce, 'RunOnce') is True: + # operations.reboot() + return False + + # Broker connection is initialized, now get information about what to + # do + counter = 0 + while self.isAlive: + try: + logger.debug('Requesting information of what to do now') + info = self.api.information() + data = info.split('\r') + if len(data) != 2: + logger.error('The format of the information message is not correct (got {})'.format(info)) + raise Exception + params = data[1].split('\t') + if data[0] == 'rename': + try: + if len(params) == 1: # Simple rename + logger.debug('Renaming computer to {}'.format(params[0])) + self.rename(params[0]) + # Rename with change password for an user + elif len(params) == 4: + logger.debug('Renaming computer to {}'.format(params)) + self.rename(params[0], params[1], params[2], params[3]) + else: + logger.error('Got invalid parameter for rename operation: {}'.format(params)) + return False + break + except Exception as e: + logger.error('Error at computer renaming stage: {}'.format(e.message)) + return None # Will retry complete broker connection if this point is reached + elif data[0] == 'domain': + if len(params) != 5: + logger.error('Got invalid parameters for domain message: {}'.format(params)) + return False # Stop running service + self.joinDomain(params[0], params[1], params[2], params[3], params[4]) + break + else: + logger.error('Unrecognized action sent from broker: {}'.format(data[0])) + return False # Stop running service + except REST.UserServiceNotFoundError: + logger.error('The host has lost the sync state with broker! (host uuid changed?)') + return False + except Exception as err: + if counter % 60 == 0: + logger.warn('Too many retries in progress, though still trying (last error: {})'.format(exceptionToMessage(err))) + counter += 1 + # Any other error is expectable and recoverable, so let's wait + # a bit and retry again + # Wait a bit before next check + self.doWait(5000) + + if self.rebootRequested: + try: + operations.reboot() + except Exception as e: + logger.error('Exception on reboot: {}'.format(e.message)) + return False # Stops service + + return True + + def checkIpsChanged(self): + if self.api is None or self.api.uuid is None: + return # Not connected + netInfo = tuple(operations.getNetworkInfo()) + for i in netInfo: + # If at least one ip has changed + if i.mac in self.knownIps and self.knownIps[i.mac] != i.ip: + logger.info('Notifying ip change to broker (mac {}, from {} to {})'.format(i.mac, self.knownIps[i.mac], i.ip)) + try: + # Notifies all interfaces IPs + self.api.notifyIpChanges(((v.mac, v.ip) for v in netInfo)) + + # Regenerates Known ips + self.knownIps = dict(((v.mac, v.ip) for v in netInfo)) + + # And notify new listening address to broker + address = (self.knownIps[self.api.mac], self.httpServer.getPort()) + # And new listening address + self.httpServer.restart(address) + # sends notification + self.api.notifyComm(self.httpServer.getServerUrl()) + + except Exception as e: + logger.warn('Got an error notifiying IPs to broker: {} (will retry in a bit)'.format(e.message.decode('windows-1250', 'ignore'))) + + def clientMessageProcessor(self, msg, data): + logger.debug('Got message {}'.format(msg)) + if self.api is None: + logger.info('Rest api not ready') + return + + if msg == ipc.REQ_LOGIN: + self.loggedIn = True + res = self.api.login(data).split('\t') + # third parameter, if exists, sets maxSession duration to this. + # First & second parameters are ip & hostname of connection source + if len(res) >= 3: + self.api.maxSession = int(res[2]) # Third parameter is max session duration + msg = ipc.REQ_INFORMATION # Senf information, requested or not, to client on login notification + if msg == ipc.REQ_LOGOUT and self.loggedIn is True: + self.loggedIn = False + self.api.logout(data) + self.onLogout(data) + if msg == ipc.REQ_INFORMATION: + info = {} + if self.api.idle is not None: + info['idle'] = self.api.idle + if self.api.maxSession is not None: + info['maxSession'] = self.api.maxSession + self.ipc.sendInformationMessage(info) + if msg == ipc.REQ_TICKET: + d = json.loads(data) + try: + result = self.api.getTicket(d['ticketId'], d['secure']) + self.ipc.sendTicketMessage(result) + except Exception: + logger.exception('Getting ticket') + self.ipc.sendTicketMessage({'error': 'invalid ticket'}) + + def initIPC(self): + # ****************************************** + # * Initialize listener IPC & REST threads * + # ****************************************** + logger.debug('Starting IPC listener at {}'.format(IPC_PORT)) + self.ipc = ipc.ServerIPC(IPC_PORT, clientMessageProcessor=self.clientMessageProcessor) + self.ipc.start() + + if self.api.mac in self.knownIps: + address = (self.knownIps[self.api.mac], random.randrange(43900, 44000)) + logger.info('Starting REST listener at {}'.format(address)) + self.httpServer = httpserver.HTTPServerThread(address, self) + self.httpServer.start() + # And notify it to broker + self.api.notifyComm(self.httpServer.getServerUrl()) + + def endIPC(self): + # Remove IPC threads + if self.ipc is not None: + try: + self.ipc.stop() + except Exception: + logger.error('Couln\'t stop ipc server') + if self.httpServer is not None: + try: + self.httpServer.stop() + except Exception: + logger.error('Couln\'t stop REST server') + + def endAPI(self): + if self.api is not None: + try: + if self.loggedIn: + self.loggedIn = False + self.api.logout('service_stopped') + self.api.notifyComm(None) + except Exception as e: + logger.error('Couln\'t remove comms url from broker: {}'.format(e)) + + # self.notifyStop() + + # *************************************************** + # Methods that ARE overriden by linux & windows Actor + # *************************************************** + def rename(self, name, user=None, oldPassword=None, newPassword=None): + ''' + Invoked when broker requests a rename action + MUST BE OVERRIDEN + ''' + raise NotImplementedError('Method renamed has not been implemented!') + + def joinDomain(self, name, domain, ou, account, password): + ''' + Invoked when broker requests a "domain" action + MUST BE OVERRIDEN + ''' + raise NotImplementedError('Method renamed has not been implemented!') + + # **************************************** + # Methods that CAN BE overriden by actors + # **************************************** + def notifyLocal(self): + self.setReady(operations.getComputerName()) + + def doWait(self, miliseconds): + ''' + Invoked to wait a bit + CAN be OVERRIDEN + ''' + time.sleep(float(miliseconds) / 1000) + + def notifyStop(self): + ''' + Overriden to log stop + ''' + logger.info('Service is being stopped') + + def preConnect(self, user, protocol): + ''' + Invoked when received a PRE Connection request via REST + ''' + logger.debug('Pre-connect does nothing') + return 'ok' + + def onLogout(self, user): + logger.debug('On logout invoked for {}'.format(user)) diff --git a/actor/src/udsactor/store.py b/actor/src/udsactor/store.py new file mode 100644 index 000000000..6281eaa81 --- /dev/null +++ b/actor/src/udsactor/store.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +''' +@author: Adolfo Gómez, dkmaster at dkmon dot com +''' +# pylint: disable=unused-wildcard-import, wildcard-import +from __future__ import unicode_literals + +import sys +if sys.platform == 'win32': + from udsactor.windows.store import * # @UnusedWildImport +else: + from udsactor.linux.store import * # @UnusedWildImport diff --git a/actor/src/udsactor/utils.py b/actor/src/udsactor/utils.py new file mode 100644 index 000000000..9480a6ab3 --- /dev/null +++ b/actor/src/udsactor/utils.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +''' +@author: Adolfo Gómez, dkmaster at dkmon dot com +''' +from __future__ import unicode_literals + +import sys +import six + +if sys.platform == 'win32': + _fromEncoding = 'windows-1250' +else: + _fromEncoding = 'utf-8' + + +def toUnicode(msg): + try: + if not isinstance(msg, six.text_type): + if isinstance(msg, six.binary_type): + return msg.decode(_fromEncoding, 'ignore') + return six.text_type(msg) + else: + return msg + except Exception: + try: + return six.text_type(msg) + except Exception: + return '' + + +def exceptionToMessage(e): + msg = '' + for arg in e.args: + if isinstance(arg, Exception): + msg = msg + exceptionToMessage(arg) + else: + msg = msg + toUnicode(arg) + '. ' + return msg + + +class Bunch(dict): + def __init__(self, **kw): + dict.__init__(self, kw) + self.__dict__ = self + diff --git a/actor/src/udsactor/windows/SENS.py b/actor/src/udsactor/windows/SENS.py new file mode 100644 index 000000000..714e6b2ec --- /dev/null +++ b/actor/src/udsactor/windows/SENS.py @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +''' +@author: Adolfo Gómez, dkmaster at dkmon dot com +''' +# _*_ coding: iso-8859-1 _*_ + +from __future__ import unicode_literals + +import win32com.client # @UnresolvedImport, pylint: disable=import-error +import win32com.server.policy # @UnresolvedImport, pylint: disable=import-error +import os + +from udsactor.log import logger + +# based on python SENS example from +# http://timgolden.me.uk/python/win32_how_do_i/track-session-events.html + +# from Sens.h +SENSGUID_PUBLISHER = "{5fee1bd6-5b9b-11d1-8dd2-00aa004abd5e}" +SENSGUID_EVENTCLASS_LOGON = "{d5978630-5b9f-11d1-8dd2-00aa004abd5e}" + +# from EventSys.h +PROGID_EventSystem = "EventSystem.EventSystem" +PROGID_EventSubscription = "EventSystem.EventSubscription" + +IID_ISensLogon = "{d597bab3-5b9f-11d1-8dd2-00aa004abd5e}" + + +class SensLogon(win32com.server.policy.DesignatedWrapPolicy): + _com_interfaces_ = [IID_ISensLogon] + _public_methods_ = [ + 'Logon', + 'Logoff', + 'StartShell', + 'DisplayLock', + 'DisplayUnlock', + 'StartScreenSaver', + 'StopScreenSaver' + ] + + def __init__(self, service): + self._wrap_(self) + self.service = service + + def Logon(self, *args): + logger.debug('Logon event: {}'.format(args)) + if self.service.api is not None and self.service.api.isConnected: + try: + data = self.service.api.login(args[0]) + logger.debug('Data received for login: {}'.format(data)) + data = data.split('\t') + if len(data) >= 2: + logger.debug('Data is valid: {}'.format(data)) + windir = os.environ['windir'] + with open(os.path.join(windir, 'remoteip.txt'), 'w') as f: + f.write(data[0]) + with open(os.path.join(windir, 'remoteh.txt'), 'w') as f: + f.write(data[1]) + except Exception as e: + logger.fatal('Error notifying logon to server: {}'.format(e)) + + def Logoff(self, *args): + logger.debug('Logoff event: arguments: {}'.format(args)) + if self.service is not None and self.service.api is not None and self.service.api.isConnected: + try: + self.service.api.logout(args[0]) + except Exception as e: + logger.fatal('Error notifying logoff to server: {}'.format(e)) + + logger.debug('Invoking onLogout: {}'.format(self.service)) + self.service.onLogout(args[0]) + logger.debug('Invoked!!') + + def StartShell(self, *args): + # logevent('StartShell : %s' % [args]) + pass + + def DisplayLock(self, *args): + # logevent('DisplayLock : %s' % [args]) + pass + + def DisplayUnlock(self, *args): + # logevent('DisplayUnlock : %s' % [args]) + pass + + def StartScreenSaver(self, *args): + # When finished basic actor, we will use this to provide a new parameter: logout on screensaver + # This will allow to easily close sessions of idle users + # logevent('StartScreenSaver : %s' % [args]) + pass + + def StopScreenSaver(self, *args): + # logevent('StopScreenSaver : %s' % [args]) + pass + + +def logevent(msg): + logger.info(msg) + +# def register(): + # call the CoInitialize to allow the registration to run in an other + # thread + # pythoncom.CoInitialize() + + # logevent('Registring ISensLogon') + + # sl=SensLogon() + # subscription_interface=pythoncom.WrapObject(sl) + + # event_system=win32com.client.Dispatch(PROGID_EventSystem) + + # event_subscription=win32com.client.Dispatch(PROGID_EventSubscription) + # event_subscription.EventClassID=SENSGUID_EVENTCLASS_LOGON + # event_subscription.PublisherID=SENSGUID_PUBLISHER + # event_subscription.SubscriptionName='Python subscription' + # event_subscription.SubscriberInterface=subscription_interface + + # event_system.Store(PROGID_EventSubscription, event_subscription) + + # pythoncom.PumpMessages() + # #logevent('ISensLogon stopped') diff --git a/actor/src/udsactor/windows/UDSActorService.py b/actor/src/udsactor/windows/UDSActorService.py new file mode 100644 index 000000000..5ae7eb5e0 --- /dev/null +++ b/actor/src/udsactor/windows/UDSActorService.py @@ -0,0 +1,371 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +''' +@author: Adolfo Gómez, dkmaster at dkmon dot com +''' +# pylint: disable=invalid-name +import struct +import subprocess +import os +import stat + +import win32serviceutil +import win32service +import win32security +import win32net +import win32event +import win32com.client +import pythoncom +import servicemanager + +from udsactor import operations +from udsactor import store +from udsactor.service import CommonService +from udsactor.service import initCfg + +from udsactor.log import logger + +from .SENS import SensLogon +from .SENS import logevent +from .SENS import SENSGUID_EVENTCLASS_LOGON +from .SENS import SENSGUID_PUBLISHER +from .SENS import PROGID_EventSubscription +from .SENS import PROGID_EventSystem + +POST_CMD = 'c:\\windows\\post-uds.bat' + + +class UDSActorSvc(win32serviceutil.ServiceFramework, CommonService): + ''' + This class represents a Windows Service for managing actor interactions + with UDS Broker and Machine + ''' + _svc_name_ = "UDSActorNG" + _svc_display_name_ = "UDS Actor Service" + _svc_description_ = "UDS Actor Management Service" + # 'System Event Notification' is the SENS service + _svc_deps_ = ['EventLog', 'SENS'] + + def __init__(self, args): + win32serviceutil.ServiceFramework.__init__(self, args) + CommonService.__init__(self) + self.hWaitStop = win32event.CreateEvent(None, 1, 0, None) + self._user = None + + def SvcStop(self): + self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) + self.isAlive = False + win32event.SetEvent(self.hWaitStop) + + SvcShutdown = SvcStop + + def notifyStop(self): + servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE, + servicemanager.PYS_SERVICE_STOPPED, + (self._svc_name_, '')) + + def doWait(self, miliseconds): + win32event.WaitForSingleObject(self.hWaitStop, miliseconds) + + def rename(self, name, user=None, oldPassword=None, newPassword=None): + ''' + Renames the computer, and optionally sets a password for an user + before this + ''' + hostName = operations.getComputerName() + + if hostName.lower() == name.lower(): + logger.info('Computer name is now {}'.format(hostName)) + self.setReady() + return + + # Check for password change request for an user + if user is not None: + logger.info('Setting password for user {}'.format(user)) + try: + operations.changeUserPassword(user, oldPassword, newPassword) + except Exception as e: + # We stop here without even renaming computer, because the + # process has failed + raise Exception( + 'Could not change password for user {} (maybe invalid current password is configured at broker): {} '.format(user, str(e))) + + operations.renameComputer(name) + # Reboot just after renaming + logger.info('Rebooting computer to activate new name {}'.format(name)) + self.reboot() + + def oneStepJoin(self, name, domain, ou, account, password): + ''' + Ejecutes the join domain in exactly one step + ''' + currName = operations.getComputerName() + # If name is desired, simply execute multiStepJoin, because computer + # name will not change + if currName.lower() == name.lower(): + self.multiStepJoin(name, domain, ou, account, password) + else: + operations.renameComputer(name) + logger.debug('Computer renamed to {} without reboot'.format(name)) + operations.joinDomain( + domain, ou, account, password, executeInOneStep=True) + logger.debug( + 'Requested join domain {} without errors'.format(domain)) + self.reboot() + + def multiStepJoin(self, name, domain, ou, account, password): + currName = operations.getComputerName() + if currName.lower() == name.lower(): + currDomain = operations.getDomainName() + if currDomain is not None: + # logger.debug('Name: "{}" vs "{}", Domain: "{}" vs "{}"'.format(currName.lower(), name.lower(), currDomain.lower(), domain.lower())) + logger.info('Machine {} is part of domain {}'.format(name, domain)) + self.setReady() + else: + operations.joinDomain( + domain, ou, account, password, executeInOneStep=False) + self.reboot() + else: + operations.renameComputer(name) + logger.info( + 'Rebooting computer got activate new name {}'.format(name)) + self.reboot() + + def joinDomain(self, name, domain, ou, account, password): + ver = operations.getWindowsVersion() + ver = ver[0] * 10 + ver[1] + logger.debug('Starting joining domain {} with name {} (detected operating version: {})'.format( + domain, name, ver)) + # If file c:\compat.bin exists, joind domain in two steps instead one + + # Accepts one step joinDomain, also remember XP is no more supported by + # microsoft, but this also must works with it because will do a "multi + # step" join + if ver >= 60 and store.useOldJoinSystem() is False: + self.oneStepJoin(name, domain, ou, account, password) + else: + logger.info('Using multiple step join because configuration requests to do so') + self.multiStepJoin(name, domain, ou, account, password) + + def preConnect(self, user, protocol): + logger.debug('Pre connect invoked') + if protocol != 'rdp': # If connection is not using rdp, skip adding user + return 'ok' + # Well known SSID for Remote Desktop Users + REMOTE_USERS_SID = 'S-1-5-32-555' + + p = win32security.GetBinarySid(REMOTE_USERS_SID) + groupName = win32security.LookupAccountSid(None, p)[0] + + useraAlreadyInGroup = False + resumeHandle = 0 + while True: + users, _, resumeHandle = win32net.NetLocalGroupGetMembers(None, groupName, 1, resumeHandle, 32768) + if user.lower() in [u['name'].lower() for u in users]: + useraAlreadyInGroup = True + break + if resumeHandle == 0: + break + + if useraAlreadyInGroup is False: + logger.debug('User not in group, adding it') + self._user = user + try: + userSSID = win32security.LookupAccountName(None, user)[0] + win32net.NetLocalGroupAddMembers(None, groupName, 0, [{'sid': userSSID}]) + except Exception as e: + logger.error('Exception adding user to Remote Desktop Users: {}'.format(e)) + else: + self._user = None + logger.debug('User {} already in group'.format(user)) + + # Now try to run pre connect command + try: + pre_cmd = store.preApplication() + if os.path.isfile(pre_cmd): + if (os.stat(pre_cmd).st_mode & stat.S_IXUSR) != 0: + subprocess.call([pre_cmd, user, protocol]) + else: + logger.info('PRECONNECT file exists but it it is not executable (needs execution permission by root)') + else: + logger.info('PRECONNECT file not found & not executed') + except Exception as e: + # Ignore output of execution command + logger.error('Executing preconnect command give') + + return 'ok' + + def ovLogon(self, username, password): + # Compose packet for ov + ub = username.encode('utf8') + up = username.encode('utf8') + packet = struct.pack('!I', len(ub)) + ub + struct.pack('!I', len(up)) + up + # Send packet with username/password to ov pipe + operations.writeToPipe("\\\\.\\pipe\\VDSMDPipe", packet, True) + return 'done' + + def onLogout(self, user): + logger.debug('Windows onLogout invoked: {}, {}'.format(user, self._user)) + try: + REMOTE_USERS_SID = 'S-1-5-32-555' + p = win32security.GetBinarySid(REMOTE_USERS_SID) + groupName = win32security.LookupAccountSid(None, p)[0] + except Exception: + logger.error('Exception getting Windows Group') + return + + if self._user is not None: + try: + win32net.NetLocalGroupDelMembers(None, groupName, [self._user]) + except Exception as e: + logger.error('Exception removing user from Remote Desktop Users: {}'.format(e)) + + def SvcDoRun(self): # pylint: disable=too-many-statements, too-many-branches + ''' + Main service loop + ''' + try: + initCfg() + + logger.debug('running SvcDoRun') + servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE, + servicemanager.PYS_SERVICE_STARTED, + (self._svc_name_, '')) + + # call the CoInitialize to allow the registration to run in an other + # thread + logger.debug('Initializing com...') + pythoncom.CoInitialize() + + # ******************************************************** + # * Ask brokers what to do before proceding to main loop * + # ******************************************************** + while True: + brokerConnected = self.interactWithBroker() + if brokerConnected is False: + logger.debug('Interact with broker returned false, stopping service after a while') + self.notifyStop() + win32event.WaitForSingleObject(self.hWaitStop, 5000) + return + elif brokerConnected is True: + break + + # If brokerConnected returns None, repeat the cycle + self.doWait(16000) # Wait for a looong while + + if self.interactWithBroker() is False: + logger.debug('Interact with broker returned false, stopping service after a while') + self.notifyStop() + win32event.WaitForSingleObject(self.hWaitStop, 5000) + return + + if self.isAlive is False: + logger.debug('The service is not alive after broker interaction, stopping it') + self.notifyStop() + return + + if self.rebootRequested is True: + logger.debug('Reboot has been requested, stopping service') + self.notifyStop() + return + + self.initIPC() + except Exception: # Any init exception wil be caught, service must be then restarted + logger.exception() + logger.debug('Exiting service with failure status') + os._exit(-1) # pylint: disable=protected-access + + # ******************************** + # * Registers SENS subscriptions * + # ******************************** + logevent('Registering ISensLogon') + subscription_guid = '{41099152-498E-11E4-8FD3-10FEED05884B}' + sl = SensLogon(self) + subscription_interface = pythoncom.WrapObject(sl) + + event_system = win32com.client.Dispatch(PROGID_EventSystem) + + event_subscription = win32com.client.Dispatch(PROGID_EventSubscription) + event_subscription.EventClassID = SENSGUID_EVENTCLASS_LOGON + event_subscription.PublisherID = SENSGUID_PUBLISHER + event_subscription.SubscriptionName = 'UDS Actor subscription' + event_subscription.SubscriptionID = subscription_guid + event_subscription.SubscriberInterface = subscription_interface + + event_system.Store(PROGID_EventSubscription, event_subscription) + + logger.debug('Registered SENS, running main loop') + + # Execute script in c:\\windows\\post-uds.bat after interacting with broker, if no reboot is requested ofc + # This will be executed only when machine gets "ready" + try: + if os.path.isfile(POST_CMD): + subprocess.call([POST_CMD, ]) + else: + logger.info('POST file not found & not executed') + except Exception as e: + # Ignore output of execution command + logger.error('Executing post command give') + + # ********************* + # * Main Service loop * + # ********************* + # Counter used to check ip changes only once every 10 seconds, for + # example + counter = 0 + while self.isAlive: + counter += 1 + # Process SENS messages, This will be a bit asyncronous (1 second + # delay) + pythoncom.PumpWaitingMessages() + if counter >= 15: # Once every 15 seconds + counter = 0 + try: + self.checkIpsChanged() + except Exception as e: + logger.error('Error checking ip change: {}'.format(e)) + # In milliseconds, will break + win32event.WaitForSingleObject(self.hWaitStop, 1000) + + logger.debug('Exited main loop, deregistering SENS') + + # ******************************************* + # * Remove SENS subscription before exiting * + # ******************************************* + event_system.Remove( + PROGID_EventSubscription, "SubscriptionID == " + subscription_guid) + + self.endIPC() # Ends IPC servers + self.endAPI() # And deinitializes REST api if needed + + self.notifyStop() + + +if __name__ == '__main__': + initCfg() + win32serviceutil.HandleCommandLine(UDSActorSvc) diff --git a/actor/src/udsactor/windows/__init__.py b/actor/src/udsactor/windows/__init__.py new file mode 100644 index 000000000..3a98c7807 --- /dev/null +++ b/actor/src/udsactor/windows/__init__.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +''' +@author: Adolfo Gómez, dkmaster at dkmon dot com +''' +from __future__ import unicode_literals diff --git a/actor/src/udsactor/windows/log.py b/actor/src/udsactor/windows/log.py new file mode 100644 index 000000000..991cb6b8c --- /dev/null +++ b/actor/src/udsactor/windows/log.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +''' +@author: Adolfo Gómez, dkmaster at dkmon dot com +''' +from __future__ import unicode_literals + +import servicemanager # @UnresolvedImport, pylint: disable=import-error +import logging +import os +import tempfile + +# Valid logging levels, from UDS Broker (uds.core.utils.log) +OTHER, DEBUG, INFO, WARN, ERROR, FATAL = (10000 * (x + 1) for x in range(6)) + + +class LocalLogger(object): + def __init__(self): + # tempdir is different for "user application" and "service" + # service wil get c:\windows\temp, while user will get c:\users\XXX\temp + logging.basicConfig( + filename=os.path.join(tempfile.gettempdir(), 'udsactor.log'), + filemode='a', + format='%(levelname)s %(asctime)s %(message)s', + level=logging.INFO + ) + self.logger = logging.getLogger('udsactor') + self.serviceLogger = False + + def log(self, level, message): + # Debug messages are logged to a file + # our loglevels are 10000 (other), 20000 (debug), .... + # logging levels are 10 (debug), 20 (info) + # OTHER = logging.NOTSET + self.logger.log(level // 1000 - 10, message) + + if level < INFO or self.serviceLogger is False: # Only information and above will be on event log + return + + if level < WARN: # Info + servicemanager.LogInfoMsg(message) + elif level < ERROR: # WARN + servicemanager.LogWarningMsg(message) + else: # Error & Fatal + servicemanager.LogErrorMsg(message) + + def isWindows(self): + return True + + def isLinux(self): + return False diff --git a/actor/src/udsactor/windows/operations.py b/actor/src/udsactor/windows/operations.py new file mode 100644 index 000000000..eb04653d8 --- /dev/null +++ b/actor/src/udsactor/windows/operations.py @@ -0,0 +1,243 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +''' +@author: Adolfo Gómez, dkmaster at dkmon dot com +''' +from __future__ import unicode_literals + +import win32com.client # @UnresolvedImport, pylint: disable=import-error +import win32net # @UnresolvedImport, pylint: disable=import-error +import win32security # @UnresolvedImport, pylint: disable=import-error +import win32api # @UnresolvedImport, pylint: disable=import-error +import win32con # @UnresolvedImport, pylint: disable=import-error +import ctypes +from ctypes.wintypes import DWORD, LPCWSTR +import os + +from udsactor import utils +from udsactor.log import logger + + +def getErrorMessage(res=0): + # sys_fs_enc = sys.getfilesystemencoding() or 'mbcs' + msg = win32api.FormatMessage(res) + return msg.decode('windows-1250', 'ignore') + + +def getComputerName(): + return win32api.GetComputerNameEx(win32con.ComputerNamePhysicalDnsHostname) + + +def getNetworkInfo(): + obj = win32com.client.Dispatch("WbemScripting.SWbemLocator") + wmobj = obj.ConnectServer("localhost", "root\\cimv2") + adapters = wmobj.ExecQuery("Select * from Win32_NetworkAdapterConfiguration where IpEnabled=True") + try: + for obj in adapters: + for ip in obj.IPAddress: + if ':' in ip: # Is IPV6, skip this + continue + if ip is None or ip == '' or ip.startswith('169.254') or ip.startswith('0.'): # If single link ip, or no ip + continue + # logger.debug('Net config found: {}=({}, {})'.format(obj.Caption, obj.MACAddress, ip)) + yield utils.Bunch(name=obj.Caption, mac=obj.MACAddress, ip=ip) + except Exception: + return + + +def getDomainName(): + ''' + Will return the domain name if we belong a domain, else None + (if part of a network group, will also return None) + ''' + # Status: + # 0 = Unknown + # 1 = Unjoined + # 2 = Workgroup + # 3 = Domain + domain, status = win32net.NetGetJoinInformation() + if status != 3: + domain = None + + return domain + + +def getWindowsVersion(): + return win32api.GetVersionEx() + + +EWX_LOGOFF = 0x00000000 +EWX_SHUTDOWN = 0x00000001 +EWX_REBOOT = 0x00000002 +EWX_FORCE = 0x00000004 +EWX_POWEROFF = 0x00000008 +EWX_FORCEIFHUNG = 0x00000010 + + +def reboot(flags=EWX_FORCEIFHUNG | EWX_REBOOT): + hproc = win32api.GetCurrentProcess() + htok = win32security.OpenProcessToken(hproc, win32security.TOKEN_ADJUST_PRIVILEGES | win32security.TOKEN_QUERY) + privs = ((win32security.LookupPrivilegeValue(None, win32security.SE_SHUTDOWN_NAME), win32security.SE_PRIVILEGE_ENABLED),) + win32security.AdjustTokenPrivileges(htok, 0, privs) + win32api.ExitWindowsEx(flags, 0) + + +def loggoff(): + win32api.ExitWindowsEx(EWX_LOGOFF) + + +def renameComputer(newName): + # Needs admin privileges to work + if ctypes.windll.kernel32.SetComputerNameExW(DWORD(win32con.ComputerNamePhysicalDnsHostname), LPCWSTR(newName)) == 0: # @UndefinedVariable + # win32api.FormatMessage -> returns error string + # win32api.GetLastError -> returns error code + # (just put this comment here to remember to log this when logger is available) + error = getErrorMessage() + computerName = win32api.GetComputerNameEx(win32con.ComputerNamePhysicalDnsHostname) + raise Exception('Error renaming computer from {} to {}: {}'.format(computerName, newName, error)) + + +NETSETUP_JOIN_DOMAIN = 0x00000001 +NETSETUP_ACCT_CREATE = 0x00000002 +NETSETUP_ACCT_DELETE = 0x00000004 +NETSETUP_WIN9X_UPGRADE = 0x00000010 +NETSETUP_DOMAIN_JOIN_IF_JOINED = 0x00000020 +NETSETUP_JOIN_UNSECURE = 0x00000040 +NETSETUP_MACHINE_PWD_PASSED = 0x00000080 +NETSETUP_JOIN_WITH_NEW_NAME = 0x00000400 +NETSETUP_DEFER_SPN_SET = 0x1000000 + + +def joinDomain(domain, ou, account, password, executeInOneStep=False): + ''' + Joins machine to a windows domain + :param domain: Domain to join to + :param ou: Ou that will hold machine + :param account: Account used to join domain + :param password: Password of account used to join domain + :param executeInOneStep: If true, means that this machine has been renamed and wants to add NETSETUP_JOIN_WITH_NEW_NAME to request so we can do rename/join in one step. + ''' + # If account do not have domain, include it + if '@' not in account and '\\' not in account: + if '.' in domain: + account = account + '@' + domain + else: + account = domain + '\\' + account + + # Do log + flags = NETSETUP_ACCT_CREATE | NETSETUP_DOMAIN_JOIN_IF_JOINED | NETSETUP_JOIN_DOMAIN + + if executeInOneStep: + flags |= NETSETUP_JOIN_WITH_NEW_NAME + + flags = DWORD(flags) + + domain = LPCWSTR(domain) + + # Must be in format "ou=.., ..., dc=...," + ou = LPCWSTR(ou) if ou is not None and ou != '' else None + account = LPCWSTR(account) + password = LPCWSTR(password) + + res = ctypes.windll.netapi32.NetJoinDomain(None, domain, ou, account, password, flags) + # Machine found in another ou, use it and warn this on log + if res == 2224: + flags = DWORD(NETSETUP_DOMAIN_JOIN_IF_JOINED | NETSETUP_JOIN_DOMAIN) + res = ctypes.windll.netapi32.NetJoinDomain(None, domain, None, account, password, flags) + if res != 0: + # Log the error + error = getErrorMessage(res) + if res == 1355: + error = "DC Is not reachable" + logger.error('Error joining domain: {}, {}'.format(error, res)) + raise Exception('Error joining domain {}, with credentials {}/*****{}: {}, {}'.format(domain, account, ', under OU {}'.format(ou) if ou is not None else '', res, error)) + + +def changeUserPassword(user, oldPassword, newPassword): + computerName = LPCWSTR(getComputerName()) + user = LPCWSTR(user) + oldPassword = LPCWSTR(oldPassword) + newPassword = LPCWSTR(newPassword) + + res = ctypes.windll.netapi32.NetUserChangePassword(computerName, user, oldPassword, newPassword) + + if res != 0: + # Log the error, and raise exception to parent + error = getErrorMessage(res) + raise Exception('Error changing password for user {}: {} {}'.format(user.value, res, error)) + + +class LASTINPUTINFO(ctypes.Structure): + _fields_ = [ + ('cbSize', ctypes.c_uint), + ('dwTime', ctypes.c_uint), + ] + + +def initIdleDuration(atLeastSeconds): + ''' + In windows, there is no need to set screensaver + ''' + pass + + +def getIdleDuration(): + try: + lastInputInfo = LASTINPUTINFO() + lastInputInfo.cbSize = ctypes.sizeof(lastInputInfo) + if ctypes.windll.user32.GetLastInputInfo(ctypes.byref(lastInputInfo)) == 0: + return 0 + # if lastInputInfo.dwTime > 1000000000: # Value toooo high, nonsense... + # return 0 + millis = ctypes.windll.kernel32.GetTickCount() - lastInputInfo.dwTime # @UndefinedVariable + if millis < 0 or millis > 1000000000: + return 0 + return millis / 1000.0 + except Exception as e: + logger.error('Getting idle duration: {}'.format(e)) + return 0 + + +def getCurrentUser(): + ''' + Returns current logged in username + ''' + return os.environ['USERNAME'] + +def writeToPipe(pipeName, bytesPayload, waitForResponse): + # (str, bytes, bool) -> Optional[bytes] + try: + with open(pipeName, 'r+b', 0) as f: + f.write(bytesPayload) + # f.seek(0) # As recommended on intenet, but seems to work fin without thos + if waitForResponse: + return f.read() + return b'ok' + except Exception as e: + None diff --git a/actor/src/udsactor/windows/store.py b/actor/src/udsactor/windows/store.py new file mode 100644 index 000000000..fca86ad31 --- /dev/null +++ b/actor/src/udsactor/windows/store.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +''' +@author: Adolfo Gómez, dkmaster at dkmon dot com +''' +from __future__ import unicode_literals + +import pickle +from win32com.shell import shell # @UnresolvedImport, pylint: disable=import-error +try: + import winreg as wreg +except ImportError: # Python 2.7 fallback + import _winreg as wreg # @UnresolvedImport, pylint: disable=import-error +import win32security # @UnresolvedImport, pylint: disable=import-error + +DEBUG = False + + +# Can be changed to whatever we want, but registry key is protected by permissions +def encoder(data): + return data.encode('bz2') + + +def decoder(data): + return data.decode('bz2') + + +path = 'Software\\UDSActor' +baseKey = wreg.HKEY_CURRENT_USER if DEBUG is True else wreg.HKEY_LOCAL_MACHINE # @UndefinedVariable + + +def checkPermissions(): + return True if DEBUG else shell.IsUserAnAdmin() + + +def fixRegistryPermissions(handle): + if DEBUG: + return + # Fix permissions so users can't read this key + v = win32security.GetSecurityInfo(handle, win32security.SE_REGISTRY_KEY, win32security.DACL_SECURITY_INFORMATION) + dacl = v.GetSecurityDescriptorDacl() + n = 0 + # Remove all normal users access permissions to the registry key + while n < dacl.GetAceCount(): + if unicode(dacl.GetAce(n)[2]) == u'PySID:S-1-5-32-545': # Whell known Users SID + dacl.DeleteAce(n) + else: + n += 1 + win32security.SetSecurityInfo(handle, win32security.SE_REGISTRY_KEY, + win32security.DACL_SECURITY_INFORMATION | win32security.PROTECTED_DACL_SECURITY_INFORMATION, + None, None, dacl, None) + + +def readConfig(): + try: + key = wreg.OpenKey(baseKey, path, 0, wreg.KEY_QUERY_VALUE) # @UndefinedVariable + data, _ = wreg.QueryValueEx(key, '') # @UndefinedVariable + wreg.CloseKey(key) # @UndefinedVariable + return pickle.loads(decoder(data)) + except Exception: + return None + + +def writeConfig(data, fixPermissions=True): + try: + key = wreg.OpenKey(baseKey, path, 0, wreg.KEY_ALL_ACCESS) # @UndefinedVariable + except Exception: + key = wreg.CreateKeyEx(baseKey, path, 0, wreg.KEY_ALL_ACCESS) # @UndefinedVariable + if fixPermissions is True: + fixRegistryPermissions(key.handle) + + wreg.SetValueEx(key, "", 0, wreg.REG_BINARY, encoder(pickle.dumps(data))) # @UndefinedVariable + wreg.CloseKey(key) # @UndefinedVariable + + +def useOldJoinSystem(): + try: + key = wreg.OpenKey(baseKey, 'Software\\UDSEnterpriseActor', 0, wreg.KEY_QUERY_VALUE) # @UndefinedVariable + try: + data, _ = wreg.QueryValueEx(key, 'join') # @UndefinedVariable + except Exception: + data = '' + wreg.CloseKey(key) # @UndefinedVariable + except: + data = '' + + return data == 'old' + + +# Gives the oportunity to run an application ONE TIME (because, the registry key "run" will be deleted after read) +def runApplication(): + try: + key = wreg.OpenKey(baseKey, 'Software\\UDSEnterpriseActor', 0, wreg.KEY_ALL_ACCESS) # @UndefinedVariable + try: + data, _ = wreg.QueryValueEx(key, 'run') # @UndefinedVariable + wreg.DeleteValue(key, 'run') # @UndefinedVariable + except Exception: + data = None + wreg.CloseKey(key) # @UndefinedVariable + except Exception: + data = None + + return data + + +def preApplication(): + try: + key = wreg.OpenKey(baseKey, 'Software\\UDSEnterpriseActor', 0, wreg.KEY_ALL_ACCESS) # @UndefinedVariable + try: + data, _ = wreg.QueryValueEx(key, 'pre') # @UndefinedVariable + except Exception: + data = None + wreg.CloseKey(key) # @UndefinedVariable + except Exception: + data = None + + return data