|
Lines 1-148
Link Here
|
| 1 |
#!/usr/bin/python2.7 |
|
|
| 2 |
# -*- coding: utf-8 -*- |
| 3 |
# |
| 4 |
# Univention Common Python Library |
| 5 |
# Connections to remote UMC Servers |
| 6 |
# |
| 7 |
# Copyright 2013-2016 Univention GmbH |
| 8 |
# |
| 9 |
# http://www.univention.de/ |
| 10 |
# |
| 11 |
# All rights reserved. |
| 12 |
# |
| 13 |
# The source code of this program is made available |
| 14 |
# under the terms of the GNU Affero General Public License version 3 |
| 15 |
# (GNU AGPL V3) as published by the Free Software Foundation. |
| 16 |
# |
| 17 |
# Binary versions of this program provided by Univention to you as |
| 18 |
# well as other copyrighted, protected or trademarked materials like |
| 19 |
# Logos, graphics, fonts, specific documentations and configurations, |
| 20 |
# cryptographic keys etc. are subject to a license agreement between |
| 21 |
# you and Univention and not subject to the GNU AGPL V3. |
| 22 |
# |
| 23 |
# In the case you use this program under the terms of the GNU AGPL V3, |
| 24 |
# the program is provided in the hope that it will be useful, |
| 25 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 26 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 27 |
# GNU Affero General Public License for more details. |
| 28 |
# |
| 29 |
# You should have received a copy of the GNU Affero General Public |
| 30 |
# License with the Debian GNU/Linux or Univention distribution in file |
| 31 |
# /usr/share/common-licenses/AGPL-3; if not, see |
| 32 |
# <http://www.gnu.org/licenses/>. |
| 33 |
|
| 34 |
from httplib import HTTPSConnection, HTTPException |
| 35 |
from json import loads, dumps |
| 36 |
from socket import error as SocketError |
| 37 |
|
| 38 |
from univention.config_registry import ConfigRegistry |
| 39 |
ucr = ConfigRegistry() |
| 40 |
ucr.load() |
| 41 |
|
| 42 |
|
| 43 |
class UMCConnection(object): |
| 44 |
|
| 45 |
def __init__(self, host, username=None, password=None, error_handler=None): |
| 46 |
self._host = host |
| 47 |
self._headers = { |
| 48 |
'Content-Type': 'application/json; charset=UTF-8', |
| 49 |
'Accept': 'application/json; q=1, text/html; q=0.5; */*; q=0.1', |
| 50 |
'X-Requested-With': 'XMLHttpRequest', |
| 51 |
} |
| 52 |
self._error_handler=error_handler |
| 53 |
if username is not None: |
| 54 |
self.auth(username, password) |
| 55 |
|
| 56 |
def get_connection(self): |
| 57 |
'''Creates a new HTTPSConnection to the host''' |
| 58 |
# once keep-alive is over, the socket closes |
| 59 |
# so create a new connection on every request |
| 60 |
return HTTPSConnection(self._host) |
| 61 |
|
| 62 |
@classmethod |
| 63 |
def get_machine_connection(cls, error_handler=None): |
| 64 |
'''Creates a connection with the credentials of the local host |
| 65 |
to the DC Master''' |
| 66 |
username = '%s$' % ucr.get('hostname') |
| 67 |
password = '' |
| 68 |
try: |
| 69 |
with open('/etc/machine.secret') as machine_file: |
| 70 |
password = machine_file.readline().strip() |
| 71 |
except (OSError, IOError) as e: |
| 72 |
if error_handler: |
| 73 |
error_handler('Could not read /etc/machine.secret: %s' % e) |
| 74 |
try: |
| 75 |
connection = cls(ucr.get('ldap/master')) |
| 76 |
connection.auth(username, password) |
| 77 |
return connection |
| 78 |
except (HTTPException, SocketError) as e: |
| 79 |
if error_handler: |
| 80 |
error_handler('Could not connect to UMC on %s: %s' % (ucr.get('ldap/master'), e)) |
| 81 |
return None |
| 82 |
|
| 83 |
def auth(self, username, password, auth_type=None): |
| 84 |
'''Tries to authenticate against the host and preserves the |
| 85 |
cookie. Has to be done only once (but keep in mind that the |
| 86 |
session probably expires after 10 minutes of inactivity)''' |
| 87 |
data = self.build_data({'username' : username, 'password' : password, 'auth_type': auth_type}) |
| 88 |
con = self.get_connection() |
| 89 |
try: |
| 90 |
con.request('POST', '/umcp/auth', data, headers=self._headers) |
| 91 |
except Exception as e: |
| 92 |
# probably unreachable |
| 93 |
if self._error_handler: |
| 94 |
self._error_handler(str(e)) |
| 95 |
error_message = '%s: Authentication failed while contacting: %s' % (self._host, e) |
| 96 |
raise HTTPException(error_message) |
| 97 |
else: |
| 98 |
try: |
| 99 |
response = con.getresponse() |
| 100 |
cookie = response.getheader('set-cookie') |
| 101 |
if cookie is None: |
| 102 |
raise ValueError('No cookie') |
| 103 |
self._headers['Cookie'] = cookie # FIXME: transform Set-Cookie to Cookie |
| 104 |
except Exception as e: |
| 105 |
if self._error_handler: |
| 106 |
self._error_handler(str(e)) |
| 107 |
error_message = '%s: Authentication failed: %s' % (self._host, response.read()) |
| 108 |
raise HTTPException(error_message) |
| 109 |
|
| 110 |
def build_data(self, data, flavor=None): |
| 111 |
'''Returns a dictionary as expected by the UMC Server''' |
| 112 |
data = {'options' : data} |
| 113 |
if flavor: |
| 114 |
data['flavor'] = flavor |
| 115 |
return dumps(data) |
| 116 |
|
| 117 |
def request(self, url, data=None, flavor=None, command='command'): |
| 118 |
'''Sends a request and returns the data from the response. url |
| 119 |
as in the XML file of that UMC module. |
| 120 |
command may be anything that UMCP understands, especially: |
| 121 |
* command (default) |
| 122 |
* get (and url could be 'ucr' then) |
| 123 |
* set (and url would be '' and data could be {'locale':'de_DE'}) |
| 124 |
* upload (url could be 'udm/license/import') |
| 125 |
''' |
| 126 |
if data is None: |
| 127 |
data = {} |
| 128 |
data = self.build_data(data, flavor) |
| 129 |
con = self.get_connection() |
| 130 |
umcp_command = '/umcp/%s' % command |
| 131 |
if url: |
| 132 |
umcp_command = '%s/%s' % (umcp_command, url) |
| 133 |
con.request('POST', umcp_command, data, headers=self._headers) |
| 134 |
response = con.getresponse() |
| 135 |
if response.status != 200: |
| 136 |
error_message = '%s on %s (%s): %s' % (response.status, self._host, url, response.read()) |
| 137 |
if response.status == 403: |
| 138 |
# 403 is either command is unknown |
| 139 |
# or command is known but forbidden |
| 140 |
if self._error_handler: |
| 141 |
self._error_handler(error_message) |
| 142 |
raise NotImplementedError('command forbidden: %s' % url) |
| 143 |
raise HTTPException(error_message) |
| 144 |
content = response.read() |
| 145 |
content = loads(content) # FIXME: inspect Content-Type response header |
| 146 |
if isinstance(content, dict): |
| 147 |
return content.get('result', content) |
| 148 |
return content |