Quellcode durchsuchen

Fix provisioning API and some other things

Tulir Asokan vor 4 Jahren
Ursprung
Commit
abbbfd4de2

+ 2 - 1
ROADMAP.md

@@ -15,7 +15,7 @@
   * [ ] Typing notifications
   * [ ] Read receipts
 * Instagram → Matrix
-  * [ ] Message content
+  * [x] Message content
     * [x] Text
     * [x] Media
       * [x] Images
@@ -43,5 +43,6 @@
   * [ ] Option to use own Matrix account for messages sent from other Instagram clients
     * [x] Automatic login with shared secret
     * [ ] Manual login with `login-matrix`
+  * [x] End-to-bridge encryption in Matrix rooms
 
 † Not supported on Instagram

+ 4 - 2
mauigpapi/http/api.py

@@ -1,8 +1,10 @@
 from .thread import ThreadAPI
-from .login_simulate import LoginSimulateAPI
 from .upload import UploadAPI
 from .challenge import ChallengeAPI
+from .account import AccountAPI
+from .qe import QeSyncAPI
+from .login import LoginAPI
 
 
-class AndroidAPI(ThreadAPI, LoginSimulateAPI, UploadAPI, ChallengeAPI):
+class AndroidAPI(ThreadAPI, AccountAPI, QeSyncAPI, LoginAPI, UploadAPI, ChallengeAPI):
     pass

+ 0 - 38
mauigpapi/http/attribution.py

@@ -1,38 +0,0 @@
-# mautrix-instagram - A Matrix-Instagram puppeting bridge.
-# Copyright (C) 2020 Tulir Asokan
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-#
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <https://www.gnu.org/licenses/>.
-from .base import BaseAndroidAPI
-from ..errors import IGResponseError
-
-
-class LogAttributionAPI(BaseAndroidAPI):
-    async def log_attribution(self):
-        # TODO parse response content
-        return await self.std_http_post("/api/v1/attribution/log_attribution/",
-                                        data={"adid": self.state.device.adid})
-
-    async def log_resurrect_attribution(self):
-        req = {
-            "_csrftoken": self.state.cookies.csrf_token,
-            "_uid": self.state.cookies.user_id,
-            "adid": self.state.device.adid,
-            "_uuid": self.state.device.uuid,
-        }
-        # Apparently this throws an error in the official app, so we catch it and return the error
-        try:
-            return await self.std_http_post("/api/v1/attribution/log_resurrect_attribution/",
-                                            data=req)
-        except IGResponseError as e:
-            return e

+ 0 - 87
mauigpapi/http/launcher.py

@@ -1,87 +0,0 @@
-# mautrix-instagram - A Matrix-Instagram puppeting bridge.
-# Copyright (C) 2020 Tulir Asokan
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-#
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <https://www.gnu.org/licenses/>.
-from typing import Dict, Any
-
-from .base import BaseAndroidAPI
-
-pre_login_configs = ("ig_fbns_blocked,ig_android_felix_release_players,"
-                     "ig_user_mismatch_soft_error,ig_android_carrier_signals_killswitch,"
-                     "ig_android_killswitch_perm_direct_ssim,fizz_ig_android,"
-                     "ig_mi_block_expired_events,ig_android_os_version_blocking_config")
-post_login_configs = ("ig_android_insights_welcome_dialog_tooltip,"
-                      "ig_android_extra_native_debugging_info,"
-                      "ig_android_insights_top_account_dialog_tooltip,"
-                      "ig_android_explore_startup_prefetch_launcher,"
-                      "ig_android_newsfeed_recyclerview,ig_android_react_native_ota_kill_switch,"
-                      "ig_qe_value_consistency_checker,"
-                      "ig_android_qp_keep_promotion_during_cooldown,"
-                      "ig_launcher_ig_explore_post_chaining_hide_comments_android_v0,"
-                      "ig_android_video_playback,"
-                      "ig_launcher_ig_android_network_stack_queue_undefined_request_qe,"
-                      "ig_camera_android_attributed_effects_endpoint_api_query_config,"
-                      "ig_android_notification_setting_sync,ig_android_dogfooding,"
-                      "ig_launcher_ig_explore_post_chaining_pill_android_v0,"
-                      "ig_android_request_compression_launcher,ig_delink_lasso_accounts,"
-                      "ig_android_stories_send_preloaded_reels_with_reels_tray,"
-                      "ig_android_critical_path_manager,"
-                      "ig_android_shopping_django_product_search,ig_android_qp_surveys_v1,"
-                      "ig_android_feed_attach_report_logs,ig_android_uri_parser_cache_launcher,"
-                      "ig_android_global_scheduler_infra,ig_android_explore_grid_viewpoint,"
-                      "ig_android_global_scheduler_direct,ig_android_upload_heap_on_oom,"
-                      "ig_launcher_ig_android_network_stack_cap_api_request_qe,"
-                      "ig_android_async_view_model_launcher,ig_android_bug_report_screen_record,"
-                      "ig_canvas_ad_pixel,ig_android_bloks_demos,"
-                      "ig_launcher_force_switch_on_dialog,ig_story_insights_entry,"
-                      "ig_android_executor_limit_per_group_config,"
-                      "ig_android_bitmap_strong_ref_cache_layer_launcher,"
-                      "ig_android_cold_start_class_preloading,"
-                      "ig_direct_e2e_send_waterfall_sample_rate_config,"
-                      "ig_android_qp_waterfall_logging,ig_synchronous_account_switch,"
-                      "ig_launcher_ig_android_reactnative_realtime_ota,"
-                      "ig_contact_invites_netego_killswitch,"
-                      "ig_launcher_ig_explore_video_chaining_container_module_android,"
-                      "ig_launcher_ig_explore_remove_topic_channel_tooltip_experiment_android,"
-                      "ig_android_request_cap_tuning_with_bandwidth,"
-                      "ig_android_rageshake_redesign,"
-                      "ig_launcher_explore_navigation_redesign_android,"
-                      "ig_android_betamap_cold_start,ig_android_employee_options,"
-                      "ig_android_direct_gifs_killswitch,ig_android_gps_improvements_launcher,"
-                      "ig_launcher_ig_android_network_stack_cap_video_request_qe,"
-                      "ig_launcher_ig_android_network_request_cap_tuning_qe,"
-                      "ig_android_qp_xshare_to_fb,ig_android_feed_report_ranking_issue,"
-                      "ig_launcher_ig_explore_verified_badge_android,"
-                      "ig_android_bloks_data_release,ig_android_feed_camera_latency")
-
-
-class LauncherSyncAPI(BaseAndroidAPI):
-    async def launcher_pre_login_sync(self):
-        await self.__sync({
-            "id": self.state.device.uuid,
-            "configs": pre_login_configs,
-        })
-
-    async def launcher_post_login_sync(self):
-        await self.__sync({
-            "_csrftoken": self.state.cookies.csrf_token,
-            "id": self.state.cookies.user_id,
-            "_uid": self.state.cookies.user_id,
-            "_uuid": self.state.device.uuid,
-            "configs": post_login_configs,
-        })
-
-    async def __sync(self, req: Dict[str, Any]):
-        # TODO parse response?
-        return await self.std_http_post("/api/v1/launcher/sync/", data=req)

+ 0 - 83
mauigpapi/http/login_simulate.py

@@ -1,83 +0,0 @@
-# mautrix-instagram - A Matrix-Instagram puppeting bridge.
-# Copyright (C) 2020 Tulir Asokan
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-#
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <https://www.gnu.org/licenses/>.
-from typing import List, Awaitable, Any
-import random
-
-from ..types import LoginResponse
-from .account import AccountAPI
-from .login import LoginAPI
-from .qe import QeSyncAPI
-from .zr import ZRTokenAPI
-from .attribution import LogAttributionAPI
-from .launcher import LauncherSyncAPI
-
-
-class LoginSimulateAPI(AccountAPI, LogAttributionAPI, QeSyncAPI, ZRTokenAPI, LoginAPI,
-                       LauncherSyncAPI):
-    @property
-    def _pre_login_flow_requests(self) -> List[Awaitable[Any]]:
-        return [
-            self.read_msisdn_header(),
-            self.msisdn_header_bootstrap("ig_select_app"),
-            self.zr_token_result(),
-            self.contact_point_prefill("prefill"),
-            self.launcher_pre_login_sync(),
-            self.qe_sync_login_experiments(),
-            self.log_attribution(),
-            self.get_prefill_candidates(),
-        ]
-
-    @property
-    def _post_login_flow_requests(self) -> List[Awaitable[Any]]:
-        return [
-            self.zr_token_result(),
-            self.launcher_post_login_sync(),
-            self.qe_sync_experiments(),
-            self.log_attribution(),
-            self.log_resurrect_attribution(),
-
-            self._facebook_ota(),
-        ]
-
-    async def simulate_pre_login_flow(self) -> None:
-        items = self._pre_login_flow_requests
-        random.shuffle(items)
-        for item in items:
-            await item
-
-    async def simulate_post_login_flow(self) -> None:
-        items = self._post_login_flow_requests
-        random.shuffle(items)
-        for item in items:
-            await item
-
-    async def _facebook_ota(self):
-        query = {
-            "fields": self.state.application.FACEBOOK_OTA_FIELDS,
-            "custom_user_id": self.state.cookies.user_id,
-            "signed_body": "SIGNATURE.",
-            "version_code": self.state.application.APP_VERSION_CODE,
-            "version_name": self.state.application.APP_VERSION,
-            "custom_app_id": self.state.application.FACEBOOK_ORCA_APPLICATION_ID,
-            "custom_device_id": self.state.device.uuid,
-        }
-        # TODO parse response?
-        return await self.std_http_get("/api/v1/facebook_ota/", query=query)
-
-    async def upgrade_login(self) -> LoginResponse:
-        user_id = self.state.cookies.user_id
-        resp = await self.logout(one_tap_app_login=True)
-        return await self.one_tap_app_login(user_id, resp.login_nonce)

+ 0 - 28
mauigpapi/http/zr.py

@@ -1,28 +0,0 @@
-# mautrix-instagram - A Matrix-Instagram puppeting bridge.
-# Copyright (C) 2020 Tulir Asokan
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-#
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <https://www.gnu.org/licenses/>.
-from .base import BaseAndroidAPI
-
-
-class ZRTokenAPI(BaseAndroidAPI):
-    async def zr_token_result(self):
-        query = {
-            "device_id": self.state.device.id,
-            "token_hash": "",
-            "custom_device_id": self.state.device.uuid,
-            "fetch_reason": "token_expired",
-        }
-        # TODO parse response content
-        return await self.std_http_get("/api/v1/zr/token/result/", query=query)

+ 2 - 2
mauigpapi/mqtt/conn.py

@@ -435,9 +435,9 @@ class AndroidMQTT:
             return await fut
 
     async def iris_subscribe(self, seq_id: int, snapshot_at_ms: int) -> None:
+        self.log.debug(f"Requesting iris subscribe {seq_id}/{snapshot_at_ms}")
         resp = await self.request(RealtimeTopic.SUB_IRIS, RealtimeTopic.SUB_IRIS_RESPONSE,
-                                  {"seq_id": seq_id, "snapshot_at_ms": snapshot_at_ms,
-                                   "snapshot_app_version": "message"})
+                                  {"seq_id": seq_id, "snapshot_at_ms": snapshot_at_ms})
         self.log.debug("Iris subscribe response: %s", resp.payload.decode("utf-8"))
 
     def graphql_subscribe(self, subs: Set[str]) -> asyncio.Future:

+ 7 - 2
mauigpapi/state/application.py

@@ -26,8 +26,13 @@ default_capabilities = json.loads(pkgutil.get_data("mauigpapi.state",
 
 @dataclass
 class AndroidApplication(SerializableAttrs['AndroidApplication']):
-    APP_VERSION: str = "167.1.0.25.120"
-    APP_VERSION_CODE: str = "259829117"
+    # TODO these newer versions make the iris subscribe stop working for some reason
+    # APP_VERSION: str = "168.0.0.40.355"
+    # APP_VERSION_CODE: str = "261079771"
+    # APP_VERSION: str = "167.1.0.25.120"
+    # APP_VERSION_CODE: str = "259829117"
+    APP_VERSION: str = "159.0.0.29.122"
+    APP_VERSION_CODE: str = "244390482"
     BREADCRUMB_KEY: str = "iN4$aGr0m"
     FACEBOOK_ANALYTICS_APPLICATION_ID: str = "567067343352427"
     FACEBOOK_OTA_FIELDS: str = (

+ 2 - 2
mauigpapi/state/device.py

@@ -13,7 +13,7 @@
 #
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <https://www.gnu.org/licenses/>.
-from typing import Optional
+from typing import Optional, Union
 from uuid import UUID
 import pkgutil
 import random
@@ -70,7 +70,7 @@ class AndroidDevice(SerializableAttrs['AndroidDevice']):
             "model": model,
         }
 
-    def generate(self, seed: str) -> None:
+    def generate(self, seed: Union[str, bytes]) -> None:
         rand = random.Random(seed)
         self.id = f"android-{''.join(rand.choices(string.hexdigits, k=16))}"
         self.descriptor = rand.choice(descriptors)

+ 4 - 0
mauigpapi/state/state.py

@@ -44,6 +44,10 @@ class AndroidState(SerializableAttrs['AndroidState']):
     _challenge_path: Optional[str] = attr.ib(default=None, metadata={"json": "challenge_path"})
     cookies: Cookies = attr.ib(factory=lambda: Cookies())
 
+    def __attrs_post_init__(self) -> None:
+        if self.application.APP_VERSION_CODE != AndroidApplication().APP_VERSION_CODE:
+            self.application = AndroidApplication()
+
     @property
     def client_session_id(self) -> str:
         return str(self._gen_temp_uuid("clientSessionId", self.client_session_id_lifetime))

+ 2 - 1
mautrix_instagram/__main__.py

@@ -58,7 +58,8 @@ class InstagramBridge(Bridge):
     def prepare_bridge(self) -> None:
         super().prepare_bridge()
         cfg = self.config["bridge.provisioning"]
-        self.provisioning_api = ProvisioningAPI(cfg["shared_secret"])
+        self.provisioning_api = ProvisioningAPI(shared_secret=cfg["shared_secret"],
+                                                device_seed=self.config["instagram.device_seed"])
         self.az.app.add_subapp(cfg["prefix"], self.provisioning_api.app)
 
     async def start(self) -> None:

+ 13 - 11
mautrix_instagram/commands/auth.py

@@ -14,6 +14,8 @@
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <https://www.gnu.org/licenses/>.
 from typing import Tuple, TYPE_CHECKING
+import hashlib
+import hmac
 
 from mautrix.bridge.commands import HelpSection, command_handler
 from mauigpapi.state import AndroidState
@@ -31,15 +33,17 @@ if TYPE_CHECKING:
 SECTION_AUTH = HelpSection("Authentication", 10, "")
 
 
-async def get_login_state(user: 'User', username: str) -> Tuple[AndroidAPI, AndroidState]:
+async def get_login_state(user: 'User', username: str, seed: str
+                          ) -> Tuple[AndroidAPI, AndroidState]:
     if user.command_status and user.command_status["action"] == "Login":
         api: AndroidAPI = user.command_status["api"]
         state: AndroidState = user.command_status["state"]
     else:
         state = AndroidState()
-        state.device.generate(username)
-        api = AndroidAPI(state)
-        await api.simulate_pre_login_flow()
+        seed = hmac.new(seed.encode("utf-8"), username.encode("utf-8"), hashlib.sha256).digest()
+        state.device.generate(seed)
+        api = AndroidAPI(state, log=user.api_log)
+        await api.qe_sync_login_experiments()
         user.command_status = {
             "action": "Login",
             "state": state,
@@ -59,7 +63,7 @@ async def login(evt: CommandEvent) -> None:
         return
     username = evt.args[0]
     password = " ".join(evt.args[1:])
-    api, state = await get_login_state(evt.sender, username)
+    api, state = await get_login_state(evt.sender, username, evt.config["instagram.device_seed"])
     try:
         resp = await api.login(username, password)
     except IGLoginTwoFactorRequiredError as e:
@@ -94,7 +98,7 @@ async def login(evt: CommandEvent) -> None:
     except IGLoginBadPasswordError:
         await evt.reply("Incorrect password")
     else:
-        await _post_login(evt, api, state, resp.logged_in_user)
+        await _post_login(evt, state, resp.logged_in_user)
 
 
 async def enter_login_2fa(evt: CommandEvent) -> None:
@@ -123,7 +127,7 @@ async def enter_login_2fa(evt: CommandEvent) -> None:
         evt.sender.command_status = None
     else:
         evt.sender.command_status = None
-        await _post_login(evt, api, state, resp.logged_in_user)
+        await _post_login(evt, state, resp.logged_in_user)
 
 
 async def enter_login_security_code(evt: CommandEvent) -> None:
@@ -144,12 +148,10 @@ async def enter_login_security_code(evt: CommandEvent) -> None:
             await evt.reply("An unknown error occurred. Please check the bridge logs.")
             return
         evt.sender.command_status = None
-        await _post_login(evt, api, state, resp.logged_in_user)
+        await _post_login(evt, state, resp.logged_in_user)
 
 
-async def _post_login(evt: CommandEvent, api: AndroidAPI, state: AndroidState,
-                      user: BaseResponseUser) -> None:
-    await api.simulate_post_login_flow()
+async def _post_login(evt: CommandEvent, state: AndroidState, user: BaseResponseUser) -> None:
     evt.sender.state = state
     pl = state.device.payload
     manufacturer, model = pl["manufacturer"], pl["model"]

+ 4 - 0
mautrix_instagram/config.py

@@ -50,6 +50,10 @@ class Config(BaseBridgeConfig):
         copy("metrics.enabled")
         copy("metrics.listen_port")
 
+        copy("instagram.device_seed")
+        if base["instagram.device_seed"] == "generate":
+            base["instagram.device_seed"] = self._new_token()
+
         copy("bridge.username_template")
         copy("bridge.displayname_template")
 

+ 6 - 0
mautrix_instagram/example-config.yaml

@@ -57,6 +57,12 @@ metrics:
     enabled: false
     listen_port: 8000
 
+instagram:
+    # Seed for generating devices. This is secret because the seed is used to generate
+    # device IDs, which can apparently be used to bypass two-factor authentication after
+    # logging out, because Instagram is insecure.
+    device_seed: generate
+
 # Bridge config
 bridge:
     # Localpart template of MXIDs for Instagram users.

+ 12 - 1
mautrix_instagram/user.py

@@ -74,6 +74,7 @@ class User(DBUser, BaseUser):
         self.is_whitelisted, self.is_admin, self.permission_level = perms
         self.log = self.log.getChild(self.mxid)
         self.client = None
+        self.mqtt = None
         self.username = None
         self.dm_update_lock = asyncio.Lock()
         self._metric_value = defaultdict(lambda: False)
@@ -100,8 +101,12 @@ class User(DBUser, BaseUser):
         except Exception:
             self.log.exception("Error while connecting to Instagram")
 
+    @property
+    def api_log(self) -> TraceLogger:
+        return self.ig_base_log.getChild("http").getChild(self.mxid)
+
     async def connect(self) -> None:
-        client = AndroidAPI(self.state, log=self.ig_base_log.getChild("http").getChild(self.mxid))
+        client = AndroidAPI(self.state, log=self.api_log)
 
         try:
             resp = await client.current_user()
@@ -222,6 +227,11 @@ class User(DBUser, BaseUser):
         await self.update()
 
     async def logout(self) -> None:
+        if self.client:
+            try:
+                await self.client.logout(one_tap_app_login=False)
+            except Exception:
+                self.log.debug("Exception logging out", exc_info=True)
         if self.mqtt:
             self.mqtt.disconnect()
         self._track_metric(METRIC_CONNECTED, False)
@@ -236,6 +246,7 @@ class User(DBUser, BaseUser):
         self.client = None
         self.mqtt = None
         self.state = None
+        self.igpk = None
         self._is_logged_in = False
         await self.update()
 

+ 9 - 9
mautrix_instagram/web/provisioning_api.py

@@ -36,9 +36,10 @@ class ProvisioningAPI:
     log: TraceLogger = logging.getLogger("mau.web.provisioning")
     app: web.Application
 
-    def __init__(self, shared_secret: str) -> None:
+    def __init__(self, shared_secret: str, device_seed: str) -> None:
         self.app = web.Application()
         self.shared_secret = shared_secret
+        self.device_seed = device_seed
         self.app.router.add_get("/api/whoami", self.status)
         self.app.router.add_options("/api/login", self.login_options)
         self.app.router.add_options("/api/login/2fa", self.login_options)
@@ -113,7 +114,7 @@ class ProvisioningAPI:
         except KeyError:
             raise web.HTTPBadRequest(text='{"error": "Missing keys"}', headers=self._headers)
 
-        api, state = await get_login_state(user, username)
+        api, state = await get_login_state(user, username, self.device_seed)
         try:
             resp = await api.login(username, password)
         except IGLoginTwoFactorRequiredError as e:
@@ -133,7 +134,7 @@ class ProvisioningAPI:
         except IGLoginBadPasswordError:
             return web.json_response(data={"status": "incorrect-password"},
                                      status=403, headers=self._acao_headers)
-        return await self._finish_login(user, api, state, resp.logged_in_user)
+        return await self._finish_login(user, state, resp.logged_in_user)
 
     async def _get_user(self, request: web.Request, check_state: bool = False
                         ) -> Tuple['u.User', JSON]:
@@ -174,7 +175,7 @@ class ProvisioningAPI:
                 "status": "checkpoint",
                 "response": e.body.serialize(),
             }, status=202, headers=self._acao_headers)
-        return await self._finish_login(user, api, state, resp.logged_in_user)
+        return await self._finish_login(user, state, resp.logged_in_user)
 
     async def login_checkpoint(self, request: web.Request) -> web.Response:
         user, data = await self._get_user(request, check_state=True)
@@ -192,11 +193,10 @@ class ProvisioningAPI:
             return web.json_response(data={
                 "status": "incorrect-challenge-code",
             }, status=403, headers=self._acao_headers)
-        return await self._finish_login(user, api, state, resp.logged_in_user)
+        return await self._finish_login(user, state, resp.logged_in_user)
 
-    async def _finish_login(self, user: 'u.User', api: AndroidAPI, state: AndroidState,
-                            resp_user: BaseResponseUser) -> web.Response:
-        await api.simulate_post_login_flow()
+    async def _finish_login(self, user: 'u.User', state: AndroidState, resp_user: BaseResponseUser
+                            ) -> web.Response:
         user.state = state
         pl = state.device.payload
         manufacturer, model = pl["manufacturer"], pl["model"]
@@ -205,7 +205,7 @@ class ProvisioningAPI:
             "status": "logged-in",
             "device_displayname": f"{manufacturer} {model}",
             "user": resp_user.serialize(),
-        }, status=200, headers=self._headers)
+        }, status=200, headers=self._acao_headers)
 
     async def logout(self, request: web.Request) -> web.Response:
         user = await self.check_token(request)