Skip to content

Commit 2a1d407

Browse files
dkesskpengboy
andcommitted
makeservices: add makexmpp script
Co-authored-by: Kevin Peng <kpengboy@ocf.berkeley.edu>
1 parent 9e27fc2 commit 2a1d407

File tree

2 files changed

+256
-0
lines changed

2 files changed

+256
-0
lines changed

makeservices/makexmpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/bin/bash -eu
2+
if [[ "$(hostname)" != "tsunami" && "$(hostname)" != "dev-tsunami" ]]; then
3+
echo -e '\033[1;31mYou must run this command on tsunami.\033[0m'
4+
exit 1
5+
fi
6+
sudo -u ocfmakexmpp /opt/share/utils/makeservices/makexmpp-real

makeservices/makexmpp-real

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Creates an XMPP account with the same name as the username of the user who
4+
runs this program.
5+
6+
To prevent the user from taking control of this program and e.g., causing the
7+
program to dump core and reveal the Prosody admin credentials, this program
8+
should be run under a separate account from that of the user.
9+
10+
The password set for this new account is randomly generated; it is not
11+
user-selectable. Ths is to prevent compromised user accounts from also
12+
compromising the shell account password. Note that the XMPP server should also
13+
prevent the user from later changing the password.
14+
15+
The admin credentials are read from a config file; the path to it is stored in
16+
the CONF_FILE global. This file must be readable only by the setuid user! The
17+
Python configparser module is used to parse the config file, so the format is
18+
basically the Windows INI format.
19+
20+
Most of the code in this file was adapted from a SleekXMPP example program, see
21+
https://github.com/fritzy/SleekXMPP/blob/master/examples/admin_commands.py
22+
"""
23+
import os
24+
import random
25+
import string
26+
import sys
27+
from configparser import ConfigParser
28+
from textwrap import dedent
29+
30+
import sleekxmpp
31+
from ocflib.misc.mail import send_problem_report
32+
33+
34+
CONF_FILE = '/opt/share/makexmpp/makexmpp.conf'
35+
JID_DOMAIN = 'ocf.berkeley.edu'
36+
37+
PW_LENGTH = 24
38+
39+
40+
def read_config():
41+
"""Fetches the server admin JID and password from the config in
42+
'/opt/share/makeservices'."""
43+
conf = ConfigParser()
44+
conf.read(CONF_FILE)
45+
admin_jid = conf.get('makexmpp', 'jid')
46+
admin_pw = conf.get('makexmpp', 'passwd')
47+
return admin_jid, admin_pw
48+
49+
50+
def generate_password(length):
51+
r = random.SystemRandom()
52+
return ''.join(
53+
r.choice(string.ascii_letters + string.digits)
54+
for _ in range(length)
55+
)
56+
57+
58+
def intro_prompt():
59+
print(dedent(
60+
"""
61+
This program will create an XMPP account, if one does not already exist.
62+
A randomly-generated password will be generated for this account and
63+
displayed on-screen. Please make sure you are in an environment where
64+
nobody else will see it when it appears.
65+
66+
You can always re-run this command to reset the password if you lose it.
67+
68+
If you are ready to continue, type 'yes'.
69+
Typing anything other than yes will abort this script.
70+
"""
71+
))
72+
return input('Continue? ') == 'yes'
73+
74+
75+
class XMPPUserPasswordClient(sleekxmpp.ClientXMPP):
76+
def __init__(self, jid, password, newuser, newpassword):
77+
super().__init__(jid, password)
78+
79+
self._newuser = newuser
80+
self._newjid = newuser + '@' + JID_DOMAIN
81+
self._newpassword = newpassword
82+
83+
self.add_event_handler('session_start', self.start)
84+
85+
def start(self, event):
86+
"""
87+
Process the session_start event. We first try to change the user's
88+
password. If the account does not exist, we then create it.
89+
90+
`event` is an empty dictionary. The session_start event does not
91+
provide any additional data.
92+
"""
93+
94+
def command_error(iq, session):
95+
self['xep_0050'].terminate_command(session)
96+
self.disconnect()
97+
98+
condition = iq['error']['condition']
99+
errtext = iq['error']['text']
100+
raise Exception('{}: {}'.format(condition, errtext))
101+
102+
def adduser_coda(iq, session):
103+
errors = [
104+
note
105+
for note in iq['command']['notes']
106+
if note[0] != 'info'
107+
]
108+
if not errors:
109+
# No errors mean the user was created successfully
110+
print(dedent("""
111+
Your XMPP account has been created.
112+
113+
For instructions on using your account, please visit
114+
115+
https://www.ocf.berkeley.edu/docs/services/xmpp/
116+
117+
If you run into trouble, contact us at
118+
119+
help@ocf.berkeley.edu
120+
"""))
121+
122+
self.disconnect()
123+
else:
124+
self.disconnect()
125+
raise Exception(iq['command']['notes'])
126+
127+
def adduser_form(iq, session):
128+
form = iq['command']['form']
129+
130+
answers = {
131+
'FORM_TYPE': form['fields']['FORM_TYPE']['value'],
132+
'accountjid': self._newjid,
133+
'password': self._newpassword,
134+
'password-verify': self._newpassword,
135+
}
136+
137+
form['type'] = 'submit'
138+
form['values'] = answers
139+
140+
session['next'] = adduser_coda
141+
session['payload'] = form
142+
143+
self['xep_0050'].complete_command(session)
144+
145+
def changepassword_coda(iq, session):
146+
errors = [
147+
note
148+
for note in iq['command']['notes']
149+
if note[0] != 'info'
150+
]
151+
if not errors:
152+
# No errors means the password was changed successfully
153+
print(dedent("""
154+
Your XMPP account {} already exists. Its password was reset.
155+
156+
For details on how to connect to XMPP, visit
157+
158+
https://www.ocf.berkeley.edu/docs/services/xmpp/
159+
160+
If you run into trouble using your account, contact us at
161+
162+
help@ocf.berkeley.edu
163+
""").format(self._newjid))
164+
165+
self.disconnect()
166+
elif errors == [('error', 'User does not exist')]:
167+
# Account does not exist, create it
168+
self['xep_0133'].add_user(session={
169+
'next': adduser_form,
170+
'error': command_error
171+
})
172+
else:
173+
self.disconnect()
174+
raise Exception(iq['command']['notes'])
175+
176+
def changepassword_form(iq, session):
177+
form = iq['command']['form']
178+
179+
answers = {
180+
'FORM_TYPE': form['fields']['FORM_TYPE']['value'],
181+
'accountjid': self._newjid,
182+
'password': self._newpassword,
183+
}
184+
185+
form['type'] = 'submit'
186+
form['values'] = answers
187+
188+
session['next'] = changepassword_coda
189+
session['payload'] = form
190+
191+
self['xep_0050'].complete_command(session)
192+
193+
self['xep_0133'].change_user_password(session={
194+
'next': changepassword_form,
195+
'error': command_error
196+
})
197+
198+
199+
def main():
200+
try:
201+
username = os.environ.get('SUDO_USER')
202+
203+
if not username:
204+
raise RuntimeError('Unable to read SUDO_USER.')
205+
206+
# Read config file.
207+
admin_jid, admin_pw = read_config()
208+
209+
# Check whether the script should proceed.
210+
if not intro_prompt():
211+
print('>>> Aborted by user request.')
212+
return
213+
214+
newpassword = generate_password(PW_LENGTH)
215+
216+
# Initialize the XMPP connection and register the service admin plugin.
217+
xmpp = XMPPUserPasswordClient(
218+
admin_jid,
219+
admin_pw,
220+
username,
221+
newpassword,
222+
)
223+
xmpp.register_plugin('xep_0133') # Service Administration
224+
225+
# Connect to the XMPP server and start processing XMPP stanzas.
226+
if xmpp.connect(reattempt=False):
227+
xmpp.process(block=True)
228+
else:
229+
raise Exception('Unable to connect to XMPP server.')
230+
231+
print('>>> Your XMPP account password is: {}'.format(newpassword))
232+
except Exception as ex:
233+
send_problem_report(dedent(
234+
"""\
235+
Fatal error for user '{}'
236+
237+
{}: {}\
238+
"""
239+
).format(username, ex.__class__.__name__, ex))
240+
print(dedent(
241+
"""
242+
A fatal error was encountered during program execution.
243+
OCF staff have been notified of the problem.
244+
"""
245+
))
246+
sys.exit(1)
247+
248+
249+
if __name__ == '__main__':
250+
main()

0 commit comments

Comments
 (0)