[
  {
    "path": ".gitignore",
    "content": "venv/\n"
  },
  {
    "path": "Readme.md",
    "content": "# \"Hacking\" the (Telekom) Zyxel GPON SFP module (PMG3000-D20B)\n> The SFP can be sourced very easily and is widely available in Germany.\n\n## I just want fiber internet on my non-Telekom router\n\n[jaseg](https://github.com/jaseg/) has written a guide on this [on his blog](https://jaseg.de/blog/telekom-gpon-sfp/).\n\n## TLDR\n\nCheckout the three options for configuring your SFP.\nWhen requiring a serial number change, this can be performed by the CLI only. Note, that some SFP NICs don't support hardwireing the speed settings. In this case, you need to connect the GPON fibre link to the module to be able to access it (see https://github.com/xvzf/zyxel-gpon-sfp/issues/8).\n\n### 1. WEB UI\n1. Configure the ethernet interface the SFP is in with the IP `10.10.1.2/24`.\n2. Port-forward the SFPs web interface to your local machine via SSH: `ssh -L 127.0.0.1:8080:10.10.1.1:80 <user@router>` (maybe you need to add option '-oHostKeyAlgorithms=+ssh-dss' before '-L'to connect).\n3. Access the web-interface on `http://localhost:8080`, username `admin`, password `1234`.\n\n### 2. CLI (on the SFP)\n> Note: The PLOAM ID has to be HEX encoded, in case yours is a 10-character string, you can transform it using `python3 -c 'print(hex(\"<enter PLOAM/SLID between the qotes>\"))'`. Omit the `0x` prefix.\n\n1. Configure the ethernet interface the SFP is in with the IP `10.10.1.2/24`.\n2. SSH into the module using `admin@10.10.1.1`, password `admin`.\n3. Login into the CLI with user `admin`, password `1234`.\n4. Change the _PLOAM/SLID/Installationskennung_ by entering following commands followed by a newline:\n    - `hal`\n    - `password <PLOAM/SLID>`\n5. _Optional_: Change the serial number using `set sn ...`; the first four characters are ASCII encoded, e.g. `SCOM`, the rest is followed in hex. Within sw version 'V1.00(ABVJ.0)b3v' you need to use the whole SN as ASCII encoded string (cmd like 'set sn 534312345678').\n\n### 3. CLI (remote)\n> Note: requires Python >= 3.8\n\n```\nNAME\n    zyxel_gpon_sfp.py --sfp_addr=http://10.10.1.1\n\nSYNOPSIS\n    zyxel_gpon_sfp.py --sfp_addr=http://10.10.1.1 - COMMAND\n\nCOMMANDS\n    COMMAND is one of the following:\n\n     info\n\n     set_slid\n\n     set_sn\n```\n\n\n## Motivation\nMy ISPs ([Deutsche Telekom](https://www.telekom.de/)) FTTH offering uses on a GPON network and distributes ONUs with a 1G (or 2.5G Ethernet) for non-business customers.\nI intended to run the fiber directly into my Linux router (using one of the SFP+ ports).\nLooking at the business offerings building upon the same technology revealed SFPs distributed only business customers using the [_Digitalisierungsbox Premium 2_](https://www.telekom.de/hilfe/geraete-zubehoer/router/digitalisierungsbox/premium-2#e_745060). \nThe mentioned SFP is made by Zyxel with the identifier `PMG3000-D20B` and sold as [_Digitalisierungsbox Glasfaser Modem_](https://geschaeftskunden.telekom.de/internet-dsl/produkt/digitalisierungsbox-glasfasermodem-kaufen) (Telekom only sells it to business customers but it is available online for ~40 Euros).\n\nThe module is based on a Lantiq 98035 SoC, [datasheet](https://www.electronicsdatasheets.com/download/51c42036e34e246e4900009c.pdf?format=pdf), [link to OpenWRT forums discussion on Huawei SFP module based on the same SoC](https://forum.openwrt.org/t/support-ma5671a-sfp-gpon/48042).\n\n## Accessing the module\n\nAfter _reverse engineering_ (this time it has been a `fzf` through all files, not analysing the binaries) the firmware of _Telekom Digitalisierungsbox 2_, I've identified the IP address of the module being `10.10.1.1/24` based on a SQL statement with a comment:\n```sql\n-- BS-6456: remove marker 'RESERVED' from static IP used to access the SFP module\nUPDATE Ip SET Name=\"\" WHERE IpAddress=\"10.10.1.2\" AND Interface=\"eth1\" AND LogicalInterface=\"eth1\";\n```\n\nDigging a bit further in plaintext SQL statements reveals the credentials.\n```sql\n-- ...\nINSERT INTO SshConfiguration VALUES ( 1, 0, 5, 22, 'Access only for authorized persons!', 0, '' );\nINSERT INTO SshUser VALUES ( 1, 0, 'admin', 'admin', 0 );\n-- ...\nINSERT INTO GPONConfig VALUES ( 1, 1, '10.10.1.1', 'admin', '1234', '', '' );\n```\n\nWell, let's give it a try. SSH access sounds like a charm and is confirmed by nmap:\n```bash\nxvzf@e300 ~ % nmap 10.10.1.1\nStarting Nmap 7.80 ( https://nmap.org ) at 2022-02-02 06:31 UTC\nNmap scan report for 10.10.1.1\nHost is up (0.00079s latency).\nNot shown: 998 closed ports\nPORT   STATE SERVICE\n22/tcp open  ssh\n80/tcp open  http\nMAC Address: <redacted> (Zyxel Communications)\n\nNmap done: 1 IP address (1 host up) scanned in 4.15 seconds\n```\n\nLet's give it a try with `ssh admin@10.10.1.1`:\n```\n#######################################################\n#                                                     #\n# Please login to CLI mode. Then You can do commands. #\n#                                                     #\n#######################################################\n\nEntering character mode\nEscape character is '^]'.\n\n\nLogin: admin\nPassword: <not echoed `1234`>\nZYXEL#\nZYXEL# <not echoed `?`>\n  linuxshell  Enter linux shell\n  show        show\n  system\n  manufactory\n  config\n  mib\n  sf\n  log\n  timer\n  bsp\n  hal\n  igmp\n  omci\n  ssp\nZYXEL# show version\nProject Name:              TW2362H-CDEL\nClient Product Name:       GTO100I_SFP_ZYXEL\nInternal Product Name:     GTO100I_SFP_ZYXEL\nHardware Version:          PMG3000-D20B\nBoot Version:              V1.0.0\nClient Software Version:   V1.0.0\nInternal Software Version: V1.0.0\nBuild User:                jiangyuanqi\nBuild Time:                2021-05-08 11:28:36\nBuild Method:              export ONU=gto100i_sfp_zyxel && cd ../drv && make install && cd .. && make rootfs && make install\nGIT Info:                  TW2362H-CDEL_lantiq98035/customize/TW2362H-CDEL_lantiq98035_general_20150131:e057bd83\nZYXEL#\n```\n\nSo, we can get a linux shell, nice. My SFP is running a (very old) release of [OpenWrt](https://openwrt.org):\n```bash\nZYXEL# linuxshell\nBusyBox v1.19.4 (2014-06-30 12:00:02 CST) built-in shell (ash)\nEnter 'help' for a list of built-in commands.\n\n  _______                     ________        __\n |       |.-----.-----.-----.|  |  |  |.----.|  |_\n |   -   ||  _  |  -__|     ||  |  |  ||   _||   _|\n |_______||   __|_____|__|__||________||__|  |____|\n          |__| W I R E L E S S   F R E E D O M\n -----------------------------------------------------\n ATTITUDE ADJUSTMENT (Attitude Adjustment, 12.09_ltq)\n -----------------------------------------------------\n  * 1/4 oz Vodka      Pour all ingredients into mixing\n  * 1/4 oz Gin        tin with ice, strain into glass.\n  * 1/4 oz Amaretto\n  * 1/4 oz Triple sec\n  * 1/4 oz Peach schnapps\n  * 1/4 oz Sour mix\n  * 1 splash Cranberry juice\n -----------------------------------------------------\nadmin@SFP:~# uname -a\nLinux SFP 3.10.12 #2 Wed Jul 12 12:01:33 CST 2017 mips GNU/Linux\nadmin@SFP:~#\n```\n\n## Changing GPON Serial Number / PLOAM Password\n\n```\nZYXEL# hal\nHal#\n  linuxshell  Enter linux shell\n  show        show HAL configuration\n  sn          change ont parameters\n  password    change ont password\n  set         set ont parameters\n  to1         change ont to1 interval\n  to2         change ont to2 interval\n  berinterval change BER interval\n  sfthreshold change SF threshold\n  sdthreshold change SD threshold\n  tcont       add tcont\n  no          delete HAL item\n  gemport     add HAL item\n  reset       Reset all pon configurations\n  get         get\n  omci        omci\n  stream      stream\n  mvlanaction mvlanaction\n  uni         PPTP UNI configuration\n  mtu         MTU R/W\n  multicast   multicast configartion\n  iphost      iphost\n  init        init\n  deny        deny\n  permit      permit\n  monitor     monitor\n  mac         mac\n  storm       storm\n  print       print\n  igmp        igmp\n  mcastfilt   McastFilt\nHal# sn\n  <string> change ont serial number\nHal# password\n  <string> Formate:XXXXXXXXXXXXXXXXXXXX\n```\nThe password seems to consist of 10 bytes, entered hex encoded. This is likely the PLOAM password / SLID / _Installationskennung_ / whatever you'd like to call it. \nThe `sn` seems to change the serial number of the ONU (ONT) itself. This works, though it expects the first 4 characters to be ASCII encoded (e.g. for the Telekom Glasfasermodem 2, it likely starts with SCOM (hex:`5343 4f4d`)\n\nI assumed the CLI is using the configuration interface of OpenWRT under the hood; turns out I was right:\n```\nuci show gpon\ngpon.ploam=gpon\ngpon.ploam.nPassword=0x20 0x20 0x20 0x20 0x20 0x20 0x20 0x20 0x20 0x20\ngpon.ploam.nT01=16000\ngpon.ploam.nT02=100\ngpon.ploam.nEmergencyStopState=0\ngpon.ploam.nRogueMsgIdUpstreamReset=255\ngpon.ploam.nRogueMsgRepeatUpstreamReset=3\ngpon.ploam.nRogueMsgIdDeviceReset=255\ngpon.ploam.nRogueMsgRepeatDeviceReset=3\ngpon.ploam.nRogueEnable=0\ngpon.gtc=gpon\ngpon.gtc.bDlosEnable=0\ngpon.gtc.bDlosInversion=0\ngpon.gtc.nDlosWindowSize=0\ngpon.gtc.nDlosTriggerThreshold=0\ngpon.gtc.ePower=0\ngpon.gtc.nLaserGap=0\ngpon.gtc.nLaserOffset=0\ngpon.gtc.nLaserEnEndExt=0\ngpon.gtc.nLaserEnStartExt=0\ngpon.gtc.nDyingGaspHyst=0\ngpon.gtc.nDyingGaspMsg=0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00\ngpon.gtc.nDyingGaspEnable=0\ngpon.ethernet=gpon\ngpon.ethernet.bUNI_PortEnable0=1\ngpon.ethernet.bUNI_PortEnable1=1\ngpon.ethernet.bUNI_PortEnable2=1\ngpon.ethernet.bUNI_PortEnable3=1\ngpon.gpe=gpon\ngpon.gpe.nPeNumber=6\n```\n\n## Observing the GPON SN and Password in real time\n\n### Serial and Password\nThe `onu` command helps debugging the system:\n- `onu gtcpg`: Retrieve password\n- `onu gtcsng`: Retrieve serial number\n- `onu gtcsns`: Set serial number\n\n### Connection state\n**Connected** (`curr_state=5`)\n```bash\nadmin@SFP:~# onu ploamsg\nerrorcode=0 curr_state=5\n```\n\n**Disconnected** (`curr_state=1`):\n```bash\nadmin@SFP:~# onu ploamsg\nerrorcode=0 curr_state=1 previous_state=0 elapsed_msec=16907701\n```\n\n## Enable 2.5G\n2.5G may not be enabled by default on the SFP. Use the following command to enable 2.5 manually:\n```\nZYXEL# hal\nHal# set speed 2.5g mode full\n```\n\nYou may have to disable auto-negotation and set a fixed port speed of 2.5G on your network adapter to make it work.\n\n## How to handle with unknown passwords for Zyxel PMG3000-D20B\n\n**Disclaimer: Use this on your own risk. I am not responsible for any damages!**\n\nSometimes the default login `admin/1234` for Zyxel PMG3000-D20B does not work - this describes how to get the password and how to change it.\n\n### Get the system default password:\n\n1. Open the webinterface (`http://10.10.1.1` if the modules default ip has not been changed)\n2. Login with `guest/guest`\n3. Set the admin password to `admin/1234`: `http://10.10.1.1/cgi/set_admin?rand=0.7041383755617387&type=2&username=admin&password=31rmzl323334m&level=0` - the device responds \"1\"\n4. Open SSH, Login with `admin/admin` and for the Zyxel CLI Mode with `admin/1234`\n5. Call `linuxshell`\n6. Display device default password: `cat /var/config/.user_cfg`\n\nIf this is not successful:\n1. Write the actual config with `http://10.10.1.1/cgi/set_save?rand=0.4798344808717123` - the device responds \"1\"\n2. Display device default password: `grep -ie \"admin Password\" /var/config/mib.conf`\n\n### If you would like to change this permanently (survives a factory reset):\n\nThe device default password is stored in the uboot-env partition (mtd1). The firmware contains the needed binaries but unfortunately not the needed layout-config (/etc/fw_env.config). \nFurthermore the /etc directory is not writeable because it is a squashfs filesystem.\n\n1. Login into the device, start `linuxshell`\n2. `cd /tmp`\n3. `mkdir /tmp/mount_bind`\n4. copy the hole /etc into /tmp/mount_bind: `cp -r /etc/ /tmp/mount_bind/`\n5. mount the copy for /etc: `mount -o bind /tmp/mount_bind/etc/ /etc/`\n\nNow the /etc is (up to the next reboot) writeable, because its redirected to /tmp/mount_bind/etc.\n\nFor creating the needed fw_env.config there is already a script which is called at boot time (and normally fails because of the read-only access).\n\n1. `/etc/init.d/fw_env.sh boot`\n2. Show the uboot-env variables: `fw_printenv`\n   look for `remote_account_pwd`\n   \nIt is important that fw_printenv does not complain about checksum errors. \n**If it complains, do not continue!**\n\nBe careful changing values in the uboot-env! Laser calibration data is also stored here, they are individual for every module!\nIts a good idea to store the output of `fw_printenv` - alternatively make a backup of your mtd1 partition!\n\n1. Set the new password: `fw_setenv remote_account_pwd 1234`\n2. Restore factory-defaults: `http://10.10.1.1/cgi/set_default?rand=0.8890542929500389` - the device responds \"1\"\n3. Reboot: `http://10.10.1.1/cgi/reset_onu?rand=0.14418772918225453` - the device responds \"1\"\n\nBe happy :-)\n\n## HTTP API\n\nOnly after getting SSH access I discovered the SFP comes with a WebUI and a _sort of_ API. The CLI `zyxel_gpon_sfp.py` makes use of this API to remotely configure the PLOAM password and possibly SN (again, didn't check it).\n\n## TODO \n- [ ] Prometheus exporter\n- [ ] Integrate into OpenWRT\n"
  },
  {
    "path": "flashdumps/binwalk.out",
    "content": "\nScan Time:     2022-02-14 09:07:48\nTarget File:   /Users/xvzf/gh/xvzf/zyxel-gpon-sfp/flashdumps/mtd0\nMD5 Checksum:  073455446a6d5f611cb083897f596c2a\nSignatures:    411\n\nDECIMAL       HEXADECIMAL     DESCRIPTION\n--------------------------------------------------------------------------------\n173708        0x2A68C         CRC32 polynomial table, little endian\n\n\nScan Time:     2022-02-14 09:07:48\nTarget File:   /Users/xvzf/gh/xvzf/zyxel-gpon-sfp/flashdumps/mtd1\nMD5 Checksum:  9e8a8ec86a89b832696fc75b5202f317\nSignatures:    411\n\nDECIMAL       HEXADECIMAL     DESCRIPTION\n--------------------------------------------------------------------------------\n\n\nScan Time:     2022-02-14 09:07:48\nTarget File:   /Users/xvzf/gh/xvzf/zyxel-gpon-sfp/flashdumps/mtd2\nMD5 Checksum:  1ccc7807624108778f53ea5599acbafa\nSignatures:    411\n\nDECIMAL       HEXADECIMAL     DESCRIPTION\n--------------------------------------------------------------------------------\n512           0x200           uImage header, header size: 64 bytes, header CRC: 0x4839EC66, created: 2017-07-12 04:01:47, image size: 1184511 bytes, Data Address: 0x80002000, Entry Point: 0x80002000, data CRC: 0xD4AF7C71, OS: Linux, CPU: MIPS, image type: OS Kernel Image, compression type: lzma, image name: \"OpenWrtLinux-3.10.12-svn\"\n576           0x240           LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 3481980 bytes\n1310720       0x140000        Squashfs filesystem, little endian, version 4.0, compression:xz, size: 2289342 bytes, 727 inodes, blocksize: 262144 bytes, created: 2021-05-08 03:28:38\n\n\nScan Time:     2022-02-14 09:07:48\nTarget File:   /Users/xvzf/gh/xvzf/zyxel-gpon-sfp/flashdumps/mtd3\nMD5 Checksum:  1ccc7807624108778f53ea5599acbafa\nSignatures:    411\n\nDECIMAL       HEXADECIMAL     DESCRIPTION\n--------------------------------------------------------------------------------\n512           0x200           uImage header, header size: 64 bytes, header CRC: 0x4839EC66, created: 2017-07-12 04:01:47, image size: 1184511 bytes, Data Address: 0x80002000, Entry Point: 0x80002000, data CRC: 0xD4AF7C71, OS: Linux, CPU: MIPS, image type: OS Kernel Image, compression type: lzma, image name: \"OpenWrtLinux-3.10.12-svn\"\n576           0x240           LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 3481980 bytes\n1310720       0x140000        Squashfs filesystem, little endian, version 4.0, compression:xz, size: 2289342 bytes, 727 inodes, blocksize: 262144 bytes, created: 2021-05-08 03:28:38\n\n\nScan Time:     2022-02-14 09:07:49\nTarget File:   /Users/xvzf/gh/xvzf/zyxel-gpon-sfp/flashdumps/mtd4\nMD5 Checksum:  7a870b94b05dc6d2a37212fc0c8fa6e8\nSignatures:    411\n\nDECIMAL       HEXADECIMAL     DESCRIPTION\n--------------------------------------------------------------------------------\n0             0x0             JFFS2 filesystem, big endian\n\n\nScan Time:     2022-02-14 09:07:49\nTarget File:   /Users/xvzf/gh/xvzf/zyxel-gpon-sfp/flashdumps/mtd5\nMD5 Checksum:  40f25c5f48fbf1df0286e3a80bc05911\nSignatures:    411\n\nDECIMAL       HEXADECIMAL     DESCRIPTION\n--------------------------------------------------------------------------------\n\n\nScan Time:     2022-02-14 09:07:49\nTarget File:   /Users/xvzf/gh/xvzf/zyxel-gpon-sfp/flashdumps/mtd6\nMD5 Checksum:  08c5b23a7cf0a1d97dcdca96ceb62c5b\nSignatures:    411\n\nDECIMAL       HEXADECIMAL     DESCRIPTION\n--------------------------------------------------------------------------------\n0             0x0             Squashfs filesystem, little endian, version 4.0, compression:xz, size: 2289342 bytes, 727 inodes, blocksize: 262144 bytes, created: 2021-05-08 03:28:38\n\n\nScan Time:     2022-02-14 09:07:49\nTarget File:   /Users/xvzf/gh/xvzf/zyxel-gpon-sfp/flashdumps/mtd7\nMD5 Checksum:  41d2e2c0c0edfccf76fa1c3e38bc1cf2\nSignatures:    411\n\nDECIMAL       HEXADECIMAL     DESCRIPTION\n--------------------------------------------------------------------------------\n\n\nScan Time:     2022-02-14 09:07:49\nTarget File:   /Users/xvzf/gh/xvzf/zyxel-gpon-sfp/flashdumps/sha256.sum\nMD5 Checksum:  934ac986d9125173b5478f23e37f84dc\nSignatures:    411\n\nDECIMAL       HEXADECIMAL     DESCRIPTION\n--------------------------------------------------------------------------------\n\n"
  },
  {
    "path": "flashdumps/mtd5",
    "content": "contains serial number, PLOAM etc\n"
  },
  {
    "path": "flashdumps/mtd7",
    "content": ""
  },
  {
    "path": "flashdumps/sha256.sum",
    "content": "e7abc5af0c769c7a5f94fc54467b8c44c9a704b1b6c7390e113a5eee9cc89582  mtd0\n2e5f53b7435d2cc6be28a4c4a1ce10797a499a903ef79ff0bd7d44cb25d626b3  mtd1\n513ecc010db160f3ead5938dc2835eaa298d4aa390c5e4fdb7b8855b45067646  mtd2\n513ecc010db160f3ead5938dc2835eaa298d4aa390c5e4fdb7b8855b45067646  mtd3\n4a8f38484ae405693807c198d6dcf51037bddce5991d11d9d4a98b796ea269d2  mtd4\n720eb0caef7a4a6b5796f235b40ed0085b1af439f28592205f50abe37d7bf9c5  mtd5\n81e085ea6601838740e728b9310e6b62da54cbcbf42b02a2805625b6db3c5878  mtd6\nb5a41c3758763bbec72769fab4a2533bf2db0b6312d93d25a695f9e4b9e02260  mtd7\n"
  },
  {
    "path": "requirements.txt",
    "content": "demjson==1.6\nfire==0.4.0\nrequests==2.27.1\n"
  },
  {
    "path": "zyxel_gpon_sfp.py",
    "content": "import fire\nimport requests\nimport random\nimport demjson\nfrom binascii import hexlify\n\n\ndef is_hex(a):\n    \"\"\" checks if a is a properly formated hex string \"\"\"\n    try:\n        int(a, 16)  # Just check if we can properly parse the hex\n        return len(a) % 2 == 0  # We're dealing with strings hex encoded\n    except ValueError:\n        return False\n\n\nclass SFP:\n    def __init__(self, sfp_addr, username=\"admin\", password=\"1234\"):\n        self._sfp_addr = sfp_addr\n        self._user = username\n        self._pass = password\n\n    def _req(self, path, method=\"GET\", headers={}):\n        \"\"\" Helper to perform request on the SFPs HTTP API \"\"\"\n        full_path = f\"{self._sfp_addr}{path}\"\n        auth = (self._user, self._pass)\n\n        if method == \"GET\":\n            return requests.get(full_path, auth=auth, headers=headers)\n        elif method == \"POST\":\n            return requests.post(full_path, auth=auth, headers=headers)\n\n    def info(self):\n        resp_sn = self._req(path=\"/cgi/get_sn\")\n        resp_gpon = self._req(path=\"/cgi/get_gpon_info\")\n\n        # WARNING: This is a javascript object, not JSON...\n        test = demjson.decode(resp_sn.text) | demjson.decode(resp_gpon.text)\n\n        return test\n\n    def set_slid(self, slid, string=False):\n        # Transform to a valid parameter\n        _slid = hexlify(slid).decode(\n            \"ascii\").lower() if string else slid.lower()\n\n        if not is_hex(_slid):\n            return f\"[!] Invalid SLID `{_slid}` (HEX)\"\n\n        print(f\"[ ] Applying SLID `{_slid}` (HEX)\")\n        resp = self._req(\n            path=f\"/cgi/set_sn?mode=1&pass={_slid}\", method=\"POST\")\n        if resp.status_code == 200 and resp.text == \"1\":\n            return f\"[+] Applied SLID `{_slid}`, a reboot of the SFP is required.\"\n\n    def set_sn(self, sn, string):\n        return \"Untested\"\n        # _sn = hexlify(sn) if string else sn\n        # f not is_hex(_sn):\n        #     return f\"[!] Invalid SN `{_sn}` (HEX)\"\n\n        # print(f\"[ ] Applying SN `{_sn}` (HEX)\")\n        # resp = self._req(path=f\"/cgi/set_sn?mode=1?sn={sn}\", method=\"POST\")\n        # if resp.status_code == 200 and resp.text == \"1\":\n        #     return f\"[+] Applied SN `{_sn}`, a reboot of the SFP is required.\"\n\n\nif __name__ == \"__main__\":\n    fire.Fire(SFP)\n"
  }
]