|
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 |