canaille-globuzma/tests/conftest.py

456 lines
17 KiB
Python
Raw Normal View History

2020-08-18 15:39:34 +00:00
import datetime
import os
2021-12-20 22:57:27 +00:00
import ldap.ldapobject
2020-08-18 15:39:34 +00:00
import pytest
2021-05-06 15:25:42 +00:00
import slapd
2021-12-20 22:57:27 +00:00
from canaille import create_app
from canaille.installation import setup_ldap_tree
from canaille.ldaputils import LDAPObject
from canaille.models import AuthorizationCode
from canaille.models import Client
from canaille.models import Consent
from canaille.models import Group
from canaille.models import Token
from canaille.models import User
from cryptography.hazmat.backends import default_backend as crypto_default_backend
2020-08-28 14:07:39 +00:00
from cryptography.hazmat.primitives import serialization as crypto_serialization
from cryptography.hazmat.primitives.asymmetric import rsa
2020-08-19 08:28:28 +00:00
from flask_webtest import TestApp
2020-08-18 15:39:34 +00:00
from werkzeug.security import gen_salt
2021-05-06 15:25:42 +00:00
class CustomSlapdObject(slapd.Slapd):
def __init__(self):
2021-05-24 15:43:15 +00:00
super().__init__(
schemas=(
"core.ldif",
"cosine.ldif",
"nis.ldif",
"inetorgperson.ldif",
"schemas/oauth2-openldap.ldif",
)
)
2020-08-18 15:39:34 +00:00
def _ln_schema_files(self, *args, **kwargs):
dir_path = os.path.join(
os.path.dirname(os.path.dirname(os.path.realpath(__file__))), "schemas"
)
super()._ln_schema_files(*args, **kwargs)
super()._ln_schema_files(self.custom_schema_files, dir_path)
def gen_config(self):
previous = self.openldap_schema_files
self.openldap_schema_files += self.custom_schema_files
config = super().gen_config()
self.openldap_schema_files = previous
return config
2020-08-28 14:07:39 +00:00
@pytest.fixture(scope="session")
def keypair():
key = rsa.generate_private_key(
backend=crypto_default_backend(), public_exponent=65537, key_size=2048
)
private_key = key.private_bytes(
crypto_serialization.Encoding.PEM,
crypto_serialization.PrivateFormat.PKCS8,
crypto_serialization.NoEncryption(),
)
public_key = key.public_key().public_bytes(
crypto_serialization.Encoding.OpenSSH, crypto_serialization.PublicFormat.OpenSSH
)
return private_key, public_key
@pytest.fixture
def keypair_path(keypair, tmp_path):
private_key, public_key = keypair
private_key_path = os.path.join(tmp_path, "private.pem")
with open(private_key_path, "wb") as fd:
fd.write(private_key)
public_key_path = os.path.join(tmp_path, "public.pem")
with open(public_key_path, "wb") as fd:
fd.write(public_key)
return private_key_path, public_key_path
2020-08-18 15:39:34 +00:00
@pytest.fixture(scope="session")
def slapd_server():
slapd = CustomSlapdObject()
try:
slapd.start()
suffix_dc = slapd.suffix.split(",")[0][3:]
slapd.ldapadd(
"\n".join(
[
"dn: " + slapd.suffix,
"objectClass: dcObject",
"objectClass: organization",
"dc: " + suffix_dc,
"o: " + suffix_dc,
"",
"dn: " + slapd.root_dn,
"objectClass: applicationProcess",
"cn: " + slapd.root_cn,
2020-09-28 07:47:00 +00:00
"",
"dn: ou=users," + slapd.suffix,
"objectClass: organizationalUnit",
"ou: users",
2021-04-08 15:38:13 +00:00
"",
"dn: ou=groups," + slapd.suffix,
"objectClass: organizationalUnit",
"ou: groups",
2020-08-18 15:39:34 +00:00
]
)
+ "\n"
)
LDAPObject.root_dn = slapd.suffix
2020-09-01 15:11:30 +00:00
User.base = "ou=users"
2021-04-08 15:38:13 +00:00
Group.base = "ou=groups"
2020-08-19 08:28:28 +00:00
2020-08-18 15:39:34 +00:00
yield slapd
finally:
slapd.stop()
@pytest.fixture
def slapd_connection(slapd_server):
conn = ldap.ldapobject.SimpleLDAPObject(slapd_server.ldap_uri)
conn.protocol_version = 3
conn.simple_bind_s(slapd_server.root_dn, slapd_server.root_pw)
yield conn
conn.unbind_s()
@pytest.fixture
def configuration(slapd_server, smtpd, keypair_path):
smtpd.config.use_starttls = True
2020-08-28 14:07:39 +00:00
private_key_path, public_key_path = keypair_path
2021-11-08 17:09:05 +00:00
conf = {
"SECRET_KEY": gen_salt(24),
"OAUTH2_METADATA_FILE": "canaille/conf/oauth-authorization-server.sample.json",
"OIDC_METADATA_FILE": "canaille/conf/openid-configuration.sample.json",
2021-10-31 13:40:12 +00:00
"LOGGING": {},
"LDAP": {
"ROOT_DN": slapd_server.suffix,
"URI": slapd_server.ldap_uri,
"BIND_DN": slapd_server.root_dn,
"BIND_PW": slapd_server.root_pw,
"USER_BASE": "ou=users",
"USER_FILTER": "(|(uid={login})(cn={login}))",
"GROUP_BASE": "ou=groups",
2021-10-13 08:12:44 +00:00
"TIMEOUT": 0.1,
},
2021-12-02 17:23:14 +00:00
"ACL": {
"DEFAULT": {
"READ": ["uid", "groups"],
2021-12-06 23:07:32 +00:00
"PERMISSIONS": ["use_oidc"],
2021-12-02 17:23:14 +00:00
"WRITE": [
"mail",
"givenName",
2021-12-08 17:06:50 +00:00
"jpegPhoto",
2021-12-02 17:23:14 +00:00
"sn",
"userPassword",
"telephoneNumber",
"employeeNumber",
],
},
"ADMIN": {
"FILTER": "(|(uid=admin)(sn=admin))",
"PERMISSIONS": [
"manage_users",
"manage_oidc",
"delete_account",
"impersonate_users",
"manage_groups",
],
"WRITE": [
"groups",
],
},
"MODERATOR": {
"FILTER": "(|(uid=moderator)(sn=moderator))",
"PERMISSIONS": ["manage_users", "manage_groups", "delete_account"],
"WRITE": [
"groups",
],
},
},
"JWT": {
"PUBLIC_KEY": public_key_path,
"PRIVATE_KEY": private_key_path,
"MAPPING": {
2021-12-12 15:15:06 +00:00
"SUB": "{{ user.uid[0] }}",
"NAME": "{{ user.cn[0] }}",
"PHONE_NUMBER": "{{ user.telephoneNumber[0] }}",
"EMAIL": "{{ user.mail[0] }}",
"GIVEN_NAME": "{{ user.givenName[0] }}",
"FAMILY_NAME": "{{ user.sn[0] }}",
"PREFERRED_USERNAME": "{{ user.displayName[0] }}",
"LOCALE": "{{ user.preferredLanguage[0] }}",
"PICTURE": "{% if user.jpegPhoto %}{{ url_for('account.photo', uid=user.uid[0], field='jpegPhoto', _external=True) }}{% endif %}",
2020-08-24 08:03:48 +00:00
},
},
"SMTP": {
"HOST": smtpd.hostname,
"PORT": smtpd.port,
"TLS": True,
"LOGIN": smtpd.config.login_username,
"PASSWORD": smtpd.config.login_password,
"FROM_ADDR": "admin@mydomain.tld",
},
}
2021-11-08 17:09:05 +00:00
return conf
@pytest.fixture
def app(configuration):
os.environ["AUTHLIB_INSECURE_TRANSPORT"] = "true"
2021-11-08 17:30:31 +00:00
setup_ldap_tree(configuration)
app = create_app(configuration)
2020-08-18 15:39:34 +00:00
return app
2020-08-19 07:09:22 +00:00
@pytest.fixture
def testclient(app):
app.config["TESTING"] = True
2020-08-19 08:28:28 +00:00
return TestApp(app)
2020-08-19 07:09:22 +00:00
2020-08-18 15:39:34 +00:00
@pytest.fixture
2021-10-13 09:52:02 +00:00
def client(app, slapd_connection, other_client):
2020-08-18 15:39:34 +00:00
c = Client(
oauthClientID=gen_salt(24),
oauthClientName="Some client",
oauthClientContact="contact@mydomain.tld",
oauthClientURI="https://mydomain.tld",
oauthRedirectURIs=[
"https://mydomain.tld/redirect1",
"https://mydomain.tld/redirect2",
],
oauthLogoURI="https://mydomain.tld/logo.png",
2021-12-08 14:53:20 +00:00
oauthIssueDate=datetime.datetime.now(),
2020-08-18 15:39:34 +00:00
oauthClientSecret=gen_salt(48),
2020-08-24 08:52:21 +00:00
oauthGrantType=[
"password",
"authorization_code",
"implicit",
"hybrid",
"refresh_token",
],
2020-08-20 12:30:42 +00:00
oauthResponseType=["code", "token", "id_token"],
2021-06-03 15:24:36 +00:00
oauthScope=["openid", "profile", "groups"],
2020-08-18 15:39:34 +00:00
oauthTermsOfServiceURI="https://mydomain.tld/tos",
oauthPolicyURI="https://mydomain.tld/policy",
oauthJWKURI="https://mydomain.tld/jwk",
oauthTokenEndpointAuthMethod="client_secret_basic",
)
2021-10-13 09:52:02 +00:00
c.oauthAudience = [c.dn, other_client.dn]
c.save(slapd_connection)
return c
@pytest.fixture
def other_client(app, slapd_connection):
c = Client(
oauthClientID=gen_salt(24),
oauthClientName="Some other client",
oauthClientContact="contact@myotherdomain.tld",
oauthClientURI="https://myotherdomain.tld",
oauthRedirectURIs=[
"https://myotherdomain.tld/redirect1",
"https://myotherdomain.tld/redirect2",
],
oauthLogoURI="https://myotherdomain.tld/logo.png",
2021-12-08 14:53:20 +00:00
oauthIssueDate=datetime.datetime.now(),
2021-10-13 09:52:02 +00:00
oauthClientSecret=gen_salt(48),
oauthGrantType=[
"password",
"authorization_code",
"implicit",
"hybrid",
"refresh_token",
],
oauthResponseType=["code", "token", "id_token"],
oauthScope=["openid", "profile", "groups"],
oauthTermsOfServiceURI="https://myotherdomain.tld/tos",
oauthPolicyURI="https://myotherdomain.tld/policy",
oauthJWKURI="https://myotherdomain.tld/jwk",
oauthTokenEndpointAuthMethod="client_secret_basic",
)
c.oauthAudience = [c.dn]
2020-08-18 15:39:34 +00:00
c.save(slapd_connection)
2020-08-19 08:28:28 +00:00
2020-08-18 15:39:34 +00:00
return c
2020-08-26 14:27:08 +00:00
@pytest.fixture
def authorization(app, slapd_connection, user, client):
2020-08-26 14:27:08 +00:00
a = AuthorizationCode(
oauthCode="my-code",
oauthClient=client.dn,
2020-09-07 09:28:29 +00:00
oauthSubject=user.dn,
2020-08-26 14:27:08 +00:00
oauthRedirectURI="https://foo.bar/callback",
oauthResponseType="code",
oauthScope="openid profile",
oauthNonce="nonce",
2021-12-08 14:53:20 +00:00
oauthAuthorizationDate=datetime.datetime(2020, 1, 1),
2020-08-26 14:27:08 +00:00
oauthAuthorizationLifetime="3600",
oauthCodeChallenge="challenge",
oauthCodeChallengeMethod="method",
2020-09-17 09:10:12 +00:00
oauthRevokation="",
2020-08-26 14:27:08 +00:00
)
a.save(slapd_connection)
return a
2020-08-18 15:39:34 +00:00
@pytest.fixture
def user(app, slapd_connection):
2021-12-08 14:01:35 +00:00
User.ldap_object_classes(slapd_connection)
2021-12-08 14:53:20 +00:00
LDAPObject.ldap_object_attributes(slapd_connection)
2020-08-20 08:45:33 +00:00
u = User(
2020-10-20 09:36:58 +00:00
objectClass=["inetOrgPerson"],
2021-12-06 14:40:30 +00:00
cn="John (johnny) Doe",
2020-08-20 08:45:33 +00:00
sn="Doe",
uid="user",
2020-10-21 07:52:02 +00:00
mail="john@doe.com",
2020-10-22 15:37:01 +00:00
userPassword="{SSHA}fw9DYeF/gHTHuVMepsQzVYAkffGcU8Fz",
2020-08-20 08:45:33 +00:00
)
2020-08-18 15:39:34 +00:00
u.save(slapd_connection)
return u
2020-08-23 17:56:37 +00:00
2020-08-26 13:37:15 +00:00
@pytest.fixture
def admin(app, slapd_connection):
2021-12-08 14:01:35 +00:00
User.ldap_object_classes(slapd_connection)
2021-12-23 18:21:29 +00:00
LDAPObject.ldap_object_attributes(slapd_connection)
2020-08-26 13:37:15 +00:00
u = User(
2020-10-20 09:36:58 +00:00
objectClass=["inetOrgPerson"],
2020-08-26 13:37:15 +00:00
cn="Jane Doe",
sn="Doe",
uid="admin",
2020-10-21 07:52:02 +00:00
mail="jane@doe.com",
2020-10-22 15:37:01 +00:00
userPassword="{SSHA}Vmgh2jkD0idX3eZHf8RzGos31oerjGiU",
2020-08-26 13:37:15 +00:00
)
u.save(slapd_connection)
return u
2020-11-02 11:13:03 +00:00
@pytest.fixture
def moderator(app, slapd_connection):
2021-12-08 14:01:35 +00:00
User.ldap_object_classes(slapd_connection)
2021-12-10 16:08:43 +00:00
LDAPObject.ldap_object_attributes(slapd_connection)
2020-11-02 11:13:03 +00:00
u = User(
objectClass=["inetOrgPerson"],
cn="Jack Doe",
sn="Doe",
uid="moderator",
mail="jack@doe.com",
userPassword="{SSHA}+eHyxWqajMHsOWnhONC2vbtfNZzKTkag",
)
u.save(slapd_connection)
return u
2020-08-24 12:44:32 +00:00
@pytest.fixture
def token(slapd_connection, client, user):
t = Token(
oauthAccessToken=gen_salt(48),
2021-10-13 09:52:02 +00:00
oauthAudience=[client.dn],
oauthClient=client.dn,
2020-08-24 12:44:32 +00:00
oauthSubject=user.dn,
oauthTokenType=None,
oauthRefreshToken=gen_salt(48),
oauthScope="openid profile",
2021-12-08 14:53:20 +00:00
oauthIssueDate=datetime.datetime.now(),
2020-08-24 12:44:32 +00:00
oauthTokenLifetime=str(3600),
)
t.save(slapd_connection)
return t
2020-09-17 10:01:21 +00:00
@pytest.fixture
def consent(slapd_connection, client, user):
t = Consent(
oauthClient=client.dn,
oauthSubject=user.dn,
oauthScope=["openid", "profile"],
2021-12-08 14:53:20 +00:00
oauthIssueDate=datetime.datetime.now(),
2020-09-17 10:01:21 +00:00
)
t.save(slapd_connection)
return t
2020-08-23 17:56:37 +00:00
@pytest.fixture
def logged_user(user, testclient):
with testclient.session_transaction() as sess:
sess["user_dn"] = [user.dn]
2020-08-23 17:56:37 +00:00
return user
2020-08-26 13:37:15 +00:00
@pytest.fixture
def logged_admin(admin, testclient):
with testclient.session_transaction() as sess:
sess["user_dn"] = [admin.dn]
2020-08-26 13:37:15 +00:00
return admin
2020-09-17 08:00:39 +00:00
2020-11-02 11:13:03 +00:00
@pytest.fixture
def logged_moderator(moderator, testclient):
with testclient.session_transaction() as sess:
sess["user_dn"] = [moderator.dn]
2020-11-02 11:13:03 +00:00
return moderator
2020-09-17 08:00:39 +00:00
@pytest.fixture(autouse=True)
def cleanups(slapd_connection):
yield
try:
for consent in Consent.filter(conn=slapd_connection):
consent.delete(conn=slapd_connection)
except Exception:
pass
2021-04-08 15:38:13 +00:00
2021-06-03 10:28:45 +00:00
2021-04-08 15:38:13 +00:00
@pytest.fixture
2021-06-03 10:00:04 +00:00
def foo_group(app, user, slapd_connection):
2021-12-08 14:01:35 +00:00
Group.ldap_object_classes(slapd_connection)
g = Group(
objectClass=["groupOfNames"],
member=[user.dn],
cn="foo",
)
2021-04-08 15:38:13 +00:00
g.save(slapd_connection)
2021-06-03 10:00:04 +00:00
with app.app_context():
user.load_groups(conn=slapd_connection)
2021-06-03 15:24:36 +00:00
yield g
user._groups = []
g.delete(conn=slapd_connection)
@pytest.fixture
2021-06-03 10:00:04 +00:00
def bar_group(app, admin, slapd_connection):
2021-12-08 14:01:35 +00:00
Group.ldap_object_classes(slapd_connection)
g = Group(
objectClass=["groupOfNames"],
member=[admin.dn],
cn="bar",
)
2021-06-03 10:00:04 +00:00
g.save(slapd_connection)
with app.app_context():
admin.load_groups(conn=slapd_connection)
2021-06-03 15:24:36 +00:00
yield g
admin._groups = []
g.delete(conn=slapd_connection)
2021-12-08 17:06:50 +00:00
@pytest.fixture
def jpeg_photo():
return b"\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x01\x01,\x01,\x00\x00\xff\xfe\x00\x13Created with GIMP\xff\xe2\x02\xb0ICC_PROFILE\x00\x01\x01\x00\x00\x02\xa0lcms\x040\x00\x00mntrRGB XYZ \x07\xe5\x00\x0c\x00\x08\x00\x0f\x00\x16\x00(acspAPPL\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf6\xd6\x00\x01\x00\x00\x00\x00\xd3-lcms\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\rdesc\x00\x00\x01 \x00\x00\x00@cprt\x00\x00\x01`\x00\x00\x006wtpt\x00\x00\x01\x98\x00\x00\x00\x14chad\x00\x00\x01\xac\x00\x00\x00,rXYZ\x00\x00\x01\xd8\x00\x00\x00\x14bXYZ\x00\x00\x01\xec\x00\x00\x00\x14gXYZ\x00\x00\x02\x00\x00\x00\x00\x14rTRC\x00\x00\x02\x14\x00\x00\x00 gTRC\x00\x00\x02\x14\x00\x00\x00 bTRC\x00\x00\x02\x14\x00\x00\x00 chrm\x00\x00\x024\x00\x00\x00$dmnd\x00\x00\x02X\x00\x00\x00$dmdd\x00\x00\x02|\x00\x00\x00$mluc\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x0cenUS\x00\x00\x00$\x00\x00\x00\x1c\x00G\x00I\x00M\x00P\x00 \x00b\x00u\x00i\x00l\x00t\x00-\x00i\x00n\x00 \x00s\x00R\x00G\x00Bmluc\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x0cenUS\x00\x00\x00\x1a\x00\x00\x00\x1c\x00P\x00u\x00b\x00l\x00i\x00c\x00 \x00D\x00o\x00m\x00a\x00i\x00n\x00\x00XYZ \x00\x00\x00\x00\x00\x00\xf6\xd6\x00\x01\x00\x00\x00\x00\xd3-sf32\x00\x00\x00\x00\x00\x01\x0cB\x00\x00\x05\xde\xff\xff\xf3%\x00\x00\x07\x93\x00\x00\xfd\x90\xff\xff\xfb\xa1\xff\xff\xfd\xa2\x00\x00\x03\xdc\x00\x00\xc0nXYZ \x00\x00\x00\x00\x00\x00o\xa0\x00\x008\xf5\x00\x00\x03\x90XYZ \x00\x00\x00\x00\x00\x00$\x9f\x00\x00\x0f\x84\x00\x00\xb6\xc4XYZ \x00\x00\x00\x00\x00\x00b\x97\x00\x00\xb7\x87\x00\x00\x18\xd9para\x00\x00\x00\x00\x00\x03\x00\x00\x00\x02ff\x00\x00\xf2\xa7\x00\x00\rY\x00\x00\x13\xd0\x00\x00\n[chrm\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\xa3\xd7\x00\x00T|\x00\x00L\xcd\x00\x00\x99\x9a\x00\x00&g\x00\x00\x0f\\mluc\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x0cenUS\x00\x00\x00\x08\x00\x00\x00\x1c\x00G\x00I\x00M\x00Pmluc\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x0cenUS\x00\x00\x00\x08\x00\x00\x00\x1c\x00s\x00R\x00G\x00B\xff\xdb\x00C\x00\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\xff\xdb\x00C\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\xff\xc2\x00\x11\x08\x00\x01\x00\x01\x03\x01\x11\x00\x02\x11\x01\x03\x11\x01\xff\xc4\x00\x14\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\t\xff\xc4\x00\x14\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xda\x00\x0c\x03\x01\x00\x02\x10\x03\x10\x00\x00\x01\x7f\x0f\xff\xc4\x00\x14\x10\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xda\x00\x08\x01\x01\x00\x01\x05\x02\x7f\xff\xc4\x00\x14\x11\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xda\x00\x08\x01\x03\x01\x01?\x01\x7f\xff\xc4\x00\x14\x11\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xda\x00\x08\x01\x02\x01\x01?\x01\x7f\xff\xc4\x00\x14\x10\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xda\x00\x08\x01\x01\x00\x06?\x02\x7f\xff\xc4\x00\x14\x10\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xda\x00\x08\x01\x01\x00\x01?!\x7f\xff\xda\x00\x0c\x03\x01\x00\x02\x00\x03\x00\x00\x00\x10\x1f\xff\xc4\x00\x14\x11\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xda\x00\x08\x01\x03\x01\x01?\x10\x7f\xff\xc4\x00\x14\x11\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xda\x00\x08\x01\x02\x01\x01?\x10\x7f\xff\xc4\x00\x14\x10\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0