source: OpenRLabs-Git/deploy/rlabs-docker/web2py-rlabs/gluon/contrib/pymysql/tests/test_connection.py

main
Last change on this file was 42bd667, checked in by David Fuertes <dfuertes@…>, 4 years ago

Historial Limpio

  • Property mode set to 100755
File size: 23.7 KB
Line 
1import datetime
2import sys
3import time
4import unittest2
5import pymysql
6from pymysql.tests import base
7from pymysql._compat import text_type
8
9
10class TempUser:
11    def __init__(self, c, user, db, auth=None, authdata=None, password=None):
12        self._c = c
13        self._user = user
14        self._db = db
15        create = "CREATE USER " + user
16        if password is not None:
17            create += " IDENTIFIED BY '%s'" % password
18        elif auth is not None:
19            create += " IDENTIFIED WITH %s" % auth
20            if authdata is not None:
21                create += " AS '%s'" % authdata
22        try:
23            c.execute(create)
24            self._created = True
25        except pymysql.err.InternalError:
26            # already exists - TODO need to check the same plugin applies
27            self._created = False
28        try:
29            c.execute("GRANT SELECT ON %s.* TO %s" % (db, user))
30            self._grant = True
31        except pymysql.err.InternalError:
32            self._grant = False
33
34    def __enter__(self):
35        return self
36
37    def __exit__(self, exc_type, exc_value, traceback):
38        if self._grant:
39            self._c.execute("REVOKE SELECT ON %s.* FROM %s" % (self._db, self._user))
40        if self._created:
41            self._c.execute("DROP USER %s" % self._user)
42
43
44class TestAuthentication(base.PyMySQLTestCase):
45
46    socket_auth = False
47    socket_found = False
48    two_questions_found = False
49    three_attempts_found = False
50    pam_found = False
51    mysql_old_password_found = False
52    sha256_password_found = False
53
54    import os
55    osuser = os.environ.get('USER')
56
57    # socket auth requires the current user and for the connection to be a socket
58    # rest do grants @localhost due to incomplete logic - TODO change to @% then
59    db = base.PyMySQLTestCase.databases[0].copy()
60
61    socket_auth = db.get('unix_socket') is not None \
62                  and db.get('host') in ('localhost', '127.0.0.1')
63
64    cur = pymysql.connect(**db).cursor()
65    del db['user']
66    cur.execute("SHOW PLUGINS")
67    for r in cur:
68        if (r[1], r[2]) !=  (u'ACTIVE', u'AUTHENTICATION'):
69            continue
70        if r[3] ==  u'auth_socket.so':
71            socket_plugin_name = r[0]
72            socket_found = True
73        elif r[3] ==  u'dialog_examples.so':
74            if r[0] == 'two_questions':
75                two_questions_found =  True
76            elif r[0] == 'three_attempts':
77                three_attempts_found =  True
78        elif r[0] ==  u'pam':
79            pam_found = True
80            pam_plugin_name = r[3].split('.')[0]
81            if pam_plugin_name == 'auth_pam':
82                pam_plugin_name = 'pam'
83            # MySQL: authentication_pam
84            # https://dev.mysql.com/doc/refman/5.5/en/pam-authentication-plugin.html
85
86            # MariaDB: pam
87            # https://mariadb.com/kb/en/mariadb/pam-authentication-plugin/
88
89            # Names differ but functionality is close
90        elif r[0] ==  u'mysql_old_password':
91            mysql_old_password_found = True
92        elif r[0] ==  u'sha256_password':
93            sha256_password_found = True
94        #else:
95        #    print("plugin: %r" % r[0])
96
97    def test_plugin(self):
98        # Bit of an assumption that the current user is a native password
99        self.assertEqual('mysql_native_password', self.connections[0]._auth_plugin_name)
100
101    @unittest2.skipUnless(socket_auth, "connection to unix_socket required")
102    @unittest2.skipIf(socket_found, "socket plugin already installed")
103    def testSocketAuthInstallPlugin(self):
104        # needs plugin. lets install it.
105        cur = self.connections[0].cursor()
106        try:
107            cur.execute("install plugin auth_socket soname 'auth_socket.so'")
108            TestAuthentication.socket_found = True
109            self.socket_plugin_name = 'auth_socket'
110            self.realtestSocketAuth()
111        except pymysql.err.InternalError:
112            try:
113                cur.execute("install soname 'auth_socket'")
114                TestAuthentication.socket_found = True
115                self.socket_plugin_name = 'unix_socket'
116                self.realtestSocketAuth()
117            except pymysql.err.InternalError:
118                TestAuthentication.socket_found = False
119                raise unittest2.SkipTest('we couldn\'t install the socket plugin')
120        finally:
121            if TestAuthentication.socket_found:
122                cur.execute("uninstall plugin %s" % self.socket_plugin_name)
123
124    @unittest2.skipUnless(socket_auth, "connection to unix_socket required")
125    @unittest2.skipUnless(socket_found, "no socket plugin")
126    def testSocketAuth(self):
127        self.realtestSocketAuth()
128
129    def realtestSocketAuth(self):
130        with TempUser(self.connections[0].cursor(), TestAuthentication.osuser + '@localhost',
131                      self.databases[0]['db'], self.socket_plugin_name) as u:
132            c = pymysql.connect(user=TestAuthentication.osuser, **self.db)
133
134    class Dialog(object):
135        fail=False
136
137        def __init__(self, con):
138            self.fail=TestAuthentication.Dialog.fail
139            pass
140
141        def prompt(self, echo, prompt):
142            if self.fail:
143               self.fail=False
144               return b'bad guess at a password'
145            return self.m.get(prompt)
146
147    class DialogHandler(object):
148
149        def __init__(self, con):
150            self.con=con
151
152        def authenticate(self, pkt):
153            while True:
154                flag = pkt.read_uint8()
155                echo = (flag & 0x06) == 0x02
156                last = (flag & 0x01) == 0x01
157                prompt = pkt.read_all()
158
159                if prompt == b'Password, please:':
160                    self.con.write_packet(b'stillnotverysecret\0')
161                else:
162                    self.con.write_packet(b'no idea what to do with this prompt\0')
163                pkt = self.con._read_packet()
164                pkt.check_error()
165                if pkt.is_ok_packet() or last:
166                    break
167            return pkt
168
169    class DefectiveHandler(object):
170        def __init__(self, con):
171            self.con=con
172
173
174    @unittest2.skipUnless(socket_auth, "connection to unix_socket required")
175    @unittest2.skipIf(two_questions_found, "two_questions plugin already installed")
176    def testDialogAuthTwoQuestionsInstallPlugin(self):
177        # needs plugin. lets install it.
178        cur = self.connections[0].cursor()
179        try:
180            cur.execute("install plugin two_questions soname 'dialog_examples.so'")
181            TestAuthentication.two_questions_found = True
182            self.realTestDialogAuthTwoQuestions()
183        except pymysql.err.InternalError:
184            raise unittest2.SkipTest('we couldn\'t install the two_questions plugin')
185        finally:
186            if TestAuthentication.two_questions_found:
187                cur.execute("uninstall plugin two_questions")
188
189    @unittest2.skipUnless(socket_auth, "connection to unix_socket required")
190    @unittest2.skipUnless(two_questions_found, "no two questions auth plugin")
191    def testDialogAuthTwoQuestions(self):
192        self.realTestDialogAuthTwoQuestions()
193
194    def realTestDialogAuthTwoQuestions(self):
195        TestAuthentication.Dialog.fail=False
196        TestAuthentication.Dialog.m = {b'Password, please:': b'notverysecret',
197                                       b'Are you sure ?': b'yes, of course'}
198        with TempUser(self.connections[0].cursor(), 'pymysql_2q@localhost',
199                      self.databases[0]['db'], 'two_questions', 'notverysecret') as u:
200            with self.assertRaises(pymysql.err.OperationalError):
201                pymysql.connect(user='pymysql_2q', **self.db)
202            pymysql.connect(user='pymysql_2q', auth_plugin_map={b'dialog': TestAuthentication.Dialog}, **self.db)
203
204    @unittest2.skipUnless(socket_auth, "connection to unix_socket required")
205    @unittest2.skipIf(three_attempts_found, "three_attempts plugin already installed")
206    def testDialogAuthThreeAttemptsQuestionsInstallPlugin(self):
207        # needs plugin. lets install it.
208        cur = self.connections[0].cursor()
209        try:
210            cur.execute("install plugin three_attempts soname 'dialog_examples.so'")
211            TestAuthentication.three_attempts_found = True
212            self.realTestDialogAuthThreeAttempts()
213        except pymysql.err.InternalError:
214            raise unittest2.SkipTest('we couldn\'t install the three_attempts plugin')
215        finally:
216            if TestAuthentication.three_attempts_found:
217                cur.execute("uninstall plugin three_attempts")
218
219    @unittest2.skipUnless(socket_auth, "connection to unix_socket required")
220    @unittest2.skipUnless(three_attempts_found, "no three attempts plugin")
221    def testDialogAuthThreeAttempts(self):
222        self.realTestDialogAuthThreeAttempts()
223
224    def realTestDialogAuthThreeAttempts(self):
225        TestAuthentication.Dialog.m = {b'Password, please:': b'stillnotverysecret'}
226        TestAuthentication.Dialog.fail=True   # fail just once. We've got three attempts after all
227        with TempUser(self.connections[0].cursor(), 'pymysql_3a@localhost',
228                      self.databases[0]['db'], 'three_attempts', 'stillnotverysecret') as u:
229            pymysql.connect(user='pymysql_3a', auth_plugin_map={b'dialog': TestAuthentication.Dialog}, **self.db)
230            pymysql.connect(user='pymysql_3a', auth_plugin_map={b'dialog': TestAuthentication.DialogHandler}, **self.db)
231            with self.assertRaises(pymysql.err.OperationalError):
232                pymysql.connect(user='pymysql_3a', auth_plugin_map={b'dialog': object}, **self.db)
233
234            with self.assertRaises(pymysql.err.OperationalError):
235                pymysql.connect(user='pymysql_3a', auth_plugin_map={b'dialog': TestAuthentication.DefectiveHandler}, **self.db)
236            with self.assertRaises(pymysql.err.OperationalError):
237                pymysql.connect(user='pymysql_3a', auth_plugin_map={b'notdialogplugin': TestAuthentication.Dialog}, **self.db)
238            TestAuthentication.Dialog.m = {b'Password, please:': b'I do not know'}
239            with self.assertRaises(pymysql.err.OperationalError):
240                pymysql.connect(user='pymysql_3a', auth_plugin_map={b'dialog': TestAuthentication.Dialog}, **self.db)
241            TestAuthentication.Dialog.m = {b'Password, please:': None}
242            with self.assertRaises(pymysql.err.OperationalError):
243                pymysql.connect(user='pymysql_3a', auth_plugin_map={b'dialog': TestAuthentication.Dialog}, **self.db)
244
245    @unittest2.skipUnless(socket_auth, "connection to unix_socket required")
246    @unittest2.skipIf(pam_found, "pam plugin already installed")
247    @unittest2.skipIf(os.environ.get('PASSWORD') is None, "PASSWORD env var required")
248    @unittest2.skipIf(os.environ.get('PAMSERVICE') is None, "PAMSERVICE env var required")
249    def testPamAuthInstallPlugin(self):
250        # needs plugin. lets install it.
251        cur = self.connections[0].cursor()
252        try:
253            cur.execute("install plugin pam soname 'auth_pam.so'")
254            TestAuthentication.pam_found = True
255            self.realTestPamAuth()
256        except pymysql.err.InternalError:
257            raise unittest2.SkipTest('we couldn\'t install the auth_pam plugin')
258        finally:
259            if TestAuthentication.pam_found:
260                cur.execute("uninstall plugin pam")
261
262
263    @unittest2.skipUnless(socket_auth, "connection to unix_socket required")
264    @unittest2.skipUnless(pam_found, "no pam plugin")
265    @unittest2.skipIf(os.environ.get('PASSWORD') is None, "PASSWORD env var required")
266    @unittest2.skipIf(os.environ.get('PAMSERVICE') is None, "PAMSERVICE env var required")
267    def testPamAuth(self):
268        self.realTestPamAuth()
269
270    def realTestPamAuth(self):
271        db = self.db.copy()
272        import os
273        db['password'] = os.environ.get('PASSWORD')
274        cur = self.connections[0].cursor()
275        try:
276            cur.execute('show grants for ' + TestAuthentication.osuser + '@localhost')
277            grants = cur.fetchone()[0]
278            cur.execute('drop user ' + TestAuthentication.osuser + '@localhost')
279        except pymysql.OperationalError as e:
280            # assuming the user doesn't exist which is ok too
281            self.assertEqual(1045, e.args[0])
282            grants = None
283        with TempUser(cur, TestAuthentication.osuser + '@localhost',
284                      self.databases[0]['db'], 'pam', os.environ.get('PAMSERVICE')) as u:
285            try:
286                c = pymysql.connect(user=TestAuthentication.osuser, **db)
287                db['password'] = 'very bad guess at password'
288                with self.assertRaises(pymysql.err.OperationalError):
289                    pymysql.connect(user=TestAuthentication.osuser,
290                                    auth_plugin_map={b'mysql_cleartext_password': TestAuthentication.DefectiveHandler},
291                                    **self.db)
292            except pymysql.OperationalError as e:
293                self.assertEqual(1045, e.args[0])
294                # we had 'bad guess at password' work with pam. Well at least we get a permission denied here
295                with self.assertRaises(pymysql.err.OperationalError):
296                    pymysql.connect(user=TestAuthentication.osuser,
297                                    auth_plugin_map={b'mysql_cleartext_password': TestAuthentication.DefectiveHandler},
298                                    **self.db)
299        if grants:
300            # recreate the user
301            cur.execute(grants)
302
303    # select old_password("crummy p\tassword");
304    #| old_password("crummy p\tassword") |
305    #| 2a01785203b08770                  |
306    @unittest2.skipUnless(socket_auth, "connection to unix_socket required")
307    @unittest2.skipUnless(mysql_old_password_found, "no mysql_old_password plugin")
308    def testMySQLOldPasswordAuth(self):
309        if self.mysql_server_is(self.connections[0], (5, 7, 0)):
310            raise unittest2.SkipTest('Old passwords aren\'t supported in 5.7')
311        # pymysql.err.OperationalError: (1045, "Access denied for user 'old_pass_user'@'localhost' (using password: YES)")
312        # from login in MySQL-5.6
313        if self.mysql_server_is(self.connections[0], (5, 6, 0)):
314            raise unittest2.SkipTest('Old passwords don\'t authenticate in 5.6')
315        db = self.db.copy()
316        db['password'] = "crummy p\tassword"
317        with self.connections[0] as c:
318            # deprecated in 5.6
319            if sys.version_info[0:2] >= (3,2) and self.mysql_server_is(self.connections[0], (5, 6, 0)):
320                with self.assertWarns(pymysql.err.Warning) as cm:
321                    c.execute("SELECT OLD_PASSWORD('%s')" % db['password'])
322            else:
323                c.execute("SELECT OLD_PASSWORD('%s')" % db['password'])
324            v = c.fetchone()[0]
325            self.assertEqual(v, '2a01785203b08770')
326            # only works in MariaDB and MySQL-5.6 - can't separate out by version
327            #if self.mysql_server_is(self.connections[0], (5, 5, 0)):
328            #    with TempUser(c, 'old_pass_user@localhost',
329            #                  self.databases[0]['db'], 'mysql_old_password', '2a01785203b08770') as u:
330            #        cur = pymysql.connect(user='old_pass_user', **db).cursor()
331            #        cur.execute("SELECT VERSION()")
332            c.execute("SELECT @@secure_auth")
333            secure_auth_setting = c.fetchone()[0]
334            c.execute('set old_passwords=1')
335            # pymysql.err.Warning: 'pre-4.1 password hash' is deprecated and will be removed in a future release. Please use post-4.1 password hash instead
336            if sys.version_info[0:2] >= (3,2) and self.mysql_server_is(self.connections[0], (5, 6, 0)):
337                with self.assertWarns(pymysql.err.Warning) as cm:
338                    c.execute('set global secure_auth=0')
339            else:
340                c.execute('set global secure_auth=0')
341            with TempUser(c, 'old_pass_user@localhost',
342                          self.databases[0]['db'], password=db['password']) as u:
343                cur = pymysql.connect(user='old_pass_user', **db).cursor()
344                cur.execute("SELECT VERSION()")
345            c.execute('set global secure_auth=%r' % secure_auth_setting)
346
347    @unittest2.skipUnless(socket_auth, "connection to unix_socket required")
348    @unittest2.skipUnless(sha256_password_found, "no sha256 password authentication plugin found")
349    def testAuthSHA256(self):
350        c = self.connections[0].cursor()
351        with TempUser(c, 'pymysql_sha256@localhost',
352                      self.databases[0]['db'], 'sha256_password') as u:
353            if self.mysql_server_is(self.connections[0], (5, 7, 0)):
354                c.execute("SET PASSWORD FOR 'pymysql_sha256'@'localhost' ='Sh@256Pa33'")
355            else:
356                c.execute('SET old_passwords = 2')
357                c.execute("SET PASSWORD FOR 'pymysql_sha256'@'localhost' = PASSWORD('Sh@256Pa33')")
358            db = self.db.copy()
359            db['password'] = "Sh@256Pa33"
360            # not implemented yet so thows error
361            with self.assertRaises(pymysql.err.OperationalError):
362                pymysql.connect(user='pymysql_256', **db)
363
364class TestConnection(base.PyMySQLTestCase):
365
366    def test_utf8mb4(self):
367        """This test requires MySQL >= 5.5"""
368        arg = self.databases[0].copy()
369        arg['charset'] = 'utf8mb4'
370        conn = pymysql.connect(**arg)
371
372    def test_largedata(self):
373        """Large query and response (>=16MB)"""
374        cur = self.connections[0].cursor()
375        cur.execute("SELECT @@max_allowed_packet")
376        if cur.fetchone()[0] < 16*1024*1024 + 10:
377            print("Set max_allowed_packet to bigger than 17MB")
378            return
379        t = 'a' * (16*1024*1024)
380        cur.execute("SELECT '" + t + "'")
381        assert cur.fetchone()[0] == t
382
383    def test_autocommit(self):
384        con = self.connections[0]
385        self.assertFalse(con.get_autocommit())
386
387        cur = con.cursor()
388        cur.execute("SET AUTOCOMMIT=1")
389        self.assertTrue(con.get_autocommit())
390
391        con.autocommit(False)
392        self.assertFalse(con.get_autocommit())
393        cur.execute("SELECT @@AUTOCOMMIT")
394        self.assertEqual(cur.fetchone()[0], 0)
395
396    def test_select_db(self):
397        con = self.connections[0]
398        current_db = self.databases[0]['db']
399        other_db = self.databases[1]['db']
400
401        cur = con.cursor()
402        cur.execute('SELECT database()')
403        self.assertEqual(cur.fetchone()[0], current_db)
404
405        con.select_db(other_db)
406        cur.execute('SELECT database()')
407        self.assertEqual(cur.fetchone()[0], other_db)
408
409    def test_connection_gone_away(self):
410        """
411        http://dev.mysql.com/doc/refman/5.0/en/gone-away.html
412        http://dev.mysql.com/doc/refman/5.0/en/error-messages-client.html#error_cr_server_gone_error
413        """
414        con = self.connections[0]
415        cur = con.cursor()
416        cur.execute("SET wait_timeout=1")
417        time.sleep(2)
418        with self.assertRaises(pymysql.OperationalError) as cm:
419            cur.execute("SELECT 1+1")
420        # error occures while reading, not writing because of socket buffer.
421        #self.assertEqual(cm.exception.args[0], 2006)
422        self.assertIn(cm.exception.args[0], (2006, 2013))
423
424    def test_init_command(self):
425        conn = pymysql.connect(
426            init_command='SELECT "bar"; SELECT "baz"',
427            **self.databases[0]
428        )
429        c = conn.cursor()
430        c.execute('select "foobar";')
431        self.assertEqual(('foobar',), c.fetchone())
432        conn.close()
433        with self.assertRaises(pymysql.err.Error):
434            conn.ping(reconnect=False)
435
436    def test_read_default_group(self):
437        conn = pymysql.connect(
438            read_default_group='client',
439            **self.databases[0]
440        )
441        self.assertTrue(conn.open)
442
443    def test_context(self):
444        with self.assertRaises(ValueError):
445            c = pymysql.connect(**self.databases[0])
446            with c as cur:
447                cur.execute('create table test ( a int )')
448                c.begin()
449                cur.execute('insert into test values ((1))')
450                raise ValueError('pseudo abort')
451                c.commit()
452        c = pymysql.connect(**self.databases[0])
453        with c as cur:
454            cur.execute('select count(*) from test')
455            self.assertEqual(0, cur.fetchone()[0])
456            cur.execute('insert into test values ((1))')
457        with c as cur:
458            cur.execute('select count(*) from test')
459            self.assertEqual(1,cur.fetchone()[0])
460            cur.execute('drop table test')
461
462    def test_set_charset(self):
463        c = pymysql.connect(**self.databases[0])
464        c.set_charset('utf8')
465        # TODO validate setting here
466
467    def test_defer_connect(self):
468        import socket
469        for db in self.databases:
470            d = db.copy()
471            try:
472                sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
473                sock.connect(d['unix_socket'])
474            except KeyError:
475                sock = socket.create_connection(
476                                (d.get('host', 'localhost'), d.get('port', 3306)))
477            for k in ['unix_socket', 'host', 'port']:
478                try:
479                    del d[k]
480                except KeyError:
481                    pass
482
483            c = pymysql.connect(defer_connect=True, **d)
484            self.assertFalse(c.open)
485            c.connect(sock)
486            c.close()
487            sock.close()
488
489    @unittest2.skipUnless(sys.version_info[0:2] >= (3,2), "required py-3.2")
490    def test_no_delay_warning(self):
491        current_db = self.databases[0].copy()
492        current_db['no_delay'] =  True
493        with self.assertWarns(DeprecationWarning) as cm:
494            conn = pymysql.connect(**current_db)
495
496
497# A custom type and function to escape it
498class Foo(object):
499    value = "bar"
500
501
502def escape_foo(x, d):
503    return x.value
504
505
506class TestEscape(base.PyMySQLTestCase):
507    def test_escape_string(self):
508        con = self.connections[0]
509        cur = con.cursor()
510
511        self.assertEqual(con.escape("foo'bar"), "'foo\\'bar'")
512        # added NO_AUTO_CREATE_USER as not including it in 5.7 generates warnings
513        cur.execute("SET sql_mode='NO_BACKSLASH_ESCAPES,NO_AUTO_CREATE_USER'")
514        self.assertEqual(con.escape("foo'bar"), "'foo''bar'")
515
516    def test_escape_builtin_encoders(self):
517        con = self.connections[0]
518        cur = con.cursor()
519
520        val = datetime.datetime(2012, 3, 4, 5, 6)
521        self.assertEqual(con.escape(val, con.encoders), "'2012-03-04 05:06:00'")
522
523    def test_escape_custom_object(self):
524        con = self.connections[0]
525        cur = con.cursor()
526
527        mapping = {Foo: escape_foo}
528        self.assertEqual(con.escape(Foo(), mapping), "bar")
529
530    def test_escape_fallback_encoder(self):
531        con = self.connections[0]
532        cur = con.cursor()
533
534        class Custom(str):
535            pass
536
537        mapping = {text_type: pymysql.escape_string}
538        self.assertEqual(con.escape(Custom('foobar'), mapping), "'foobar'")
539
540    def test_escape_no_default(self):
541        con = self.connections[0]
542        cur = con.cursor()
543
544        self.assertRaises(TypeError, con.escape, 42, {})
545
546    def test_escape_dict_value(self):
547        con = self.connections[0]
548        cur = con.cursor()
549
550        mapping = con.encoders.copy()
551        mapping[Foo] = escape_foo
552        self.assertEqual(con.escape({'foo': Foo()}, mapping), {'foo': "bar"})
553
554    def test_escape_list_item(self):
555        con = self.connections[0]
556        cur = con.cursor()
557
558        mapping = con.encoders.copy()
559        mapping[Foo] = escape_foo
560        self.assertEqual(con.escape([Foo()], mapping), "(bar)")
561
562    def test_previous_cursor_not_closed(self):
563        con = self.connections[0]
564        cur1 = con.cursor()
565        cur1.execute("SELECT 1; SELECT 2")
566        cur2 = con.cursor()
567        cur2.execute("SELECT 3")
568        self.assertEqual(cur2.fetchone()[0], 3)
569
570    def test_commit_during_multi_result(self):
571        con = self.connections[0]
572        cur = con.cursor()
573        cur.execute("SELECT 1; SELECT 2")
574        con.commit()
575        cur.execute("SELECT 3")
576        self.assertEqual(cur.fetchone()[0], 3)
Note: See TracBrowser for help on using the repository browser.